flecks provides some default flecks to help with common tasks such as spinning up a web server, databases, and a react runtime with babel plugins, among many other things.
In this article, we will be reimplementing a small subset of @flecks/react
in our own isolated
application so we can learn how to build with flecks.
In practice, there's not much need to reimplement React support, you can just lean on what's already provided -- that is in fact the entire point of flecks!
This is just an illustration for the sake of understanding.
Generate some stuff
First, let's generate a little application starter:
- npm
npm init @flecks/app illustration
cd illustration
We'll create two flecks, one called react
:
- npm
npm init @flecks/fleck -w packages/react
and one called component
:
- npm
npm init @flecks/fleck -w packages/component
A quick look at build/flecks.yml
shows:
'@flecks/core':
id: illustration
'@flecks/server': {}
'@illustration/component': {}
'@illustration/react': {}
Do stuff with React
Let's go in our component
package and make some React noises. We'll create
@illustration/component/src/component.jsx
:
function Component() {
// Just for now so we don't need the React instance just yet...
return false;
}
export default Component;
and we'll import it in to @illustration/component/src/index.js
:
import Component from './component';
Start your application:
- npm
npm run start
Whoops
Uh, oh! We got an error:
ERROR in ./packages/component/src/index.js 1:0-36
Module not found: Error: Can't resolve './component' in '...'
We can see that jsx
wasn't one of the extensions that got searched! This is because of flecks's
small core philosophy. It's just a JS application by default. The whole point is that we are
about to implement React support ourselves! Let's do that.
Modify the build
We need to add the ability to discover (and compile) JSX files. We'll do this in
@illustration/react
.
flecks has a bootstrap phase where build hooks like @flecks/core.babel
are invoked. This is
in contrast to the runtime phase which is where hooks like @flecks/server.up
are invoked.
We'll use @babel/preset-react
, so let's move into @illustration/react
and add it to our
dependencies:
- npm
npm install @babel/preset-react
Let's modify @illustration/react/build/flecks.bootstrap.js
like so:
exports.dependencies = [];
exports.hooks = {
// babel config to compile JSX
'@flecks/core.babel': () => ({presets: ['@babel/preset-react']}),
// implicit extensions
'@flecks/build.extensions': () => ['.jsx'],
};
The server automatically restarts and everything comes up correct! Only thing is, we don't actually have a web server yet!
Web integration
flecks provides @flecks/web
which implements a web server and runtime upon which we may build
web applications. A React application is a specialization of a web application, so let's lean on
@flecks/web
so we don't have to completely reinvent the wheel!
Dependencies
Move into @illustration/react
and add @flecks/web
as well as react
and react-dom
:
- npm
npm install @flecks/web react react-dom
Add @flecks/web
to the dependencies in @illustration/react/build/flecks.bootstrap.js
:
exports.dependencies = ['@flecks/web'];
The server automatically restarts and starts listening as a web server! It will only serve a white page for now.
Client implementation
Before we proceed we'll need to actually do something in our new web client. Let's add some code to
@illustration/react/src/index.js
:
import React from 'react';
import {createRoot} from 'react-dom/client';
export const hooks = {
'@flecks/web/client.up': async (container) => {
// Render the root we create with react-dom
createRoot(container).render(
// What to render though..?
);
console.log('rendered');
},
};
export const
all of a suddenSince we're dealing with the runtime phase now, we get access to the nice stuff we're used to
like export const hooks
instead of exports.hooks
.
Rule of thumb: if you are in build
you're probably in the bootstrap phase. Otherwise,
you are in the runtime phase.
Now, restart your application:
The server automatically restarts yet again. You'll see that we're now getting the rendered
message in the console.
What are we doing here exactly?
What are we going to render, though? This is where hooks come into play!
Our @illustration/react
fleck needs to be able to gather and render components on behalf of
other flecks so they don't have to do all of this root rendering busywork.
We'll implement a hook: @illustration/react.roots
that will allow other flecks to implement their
React root components. We'll then collect and render them all.
'@flecks/web/client.up': async (container, flecks) => {
const results = flecks.invoke('@illustration/react.roots');
// By default `invoke` returns an object: {[fleckPath]: result, ...}
// So, we'll map it into a flat list of components and use the fleck path as the key prop:
const Components = Object.entries(results)
.map(([fleckPath, Component]) => React.createElement(Component, {key: fleckPath}));
// Finally we'll render all our components as children of the `React.StrictMode` component:
createRoot(container).render(React.createElement(React.StrictMode, {}, Components));
console.log('rendered roots: %O', results);
},
The page automatically refreshes itself and shows rendered roots: Object { }
in the console.
Getting closer!
Let's go back over to @illustration/component/src/index.js
and implement our hook:
import Component from './component';
export const hooks = {
'@illustration/react.roots': () => Component,
};
Refresh the page and you will see:
rendered roots: Object { "@illustration/component": Component() }
Awesome!
Passing React
If you remember, our original component just return false
'd up there, and we only
added react
to @illustration/react
. So how do we use it from @illustration/component
?
Well, @flecks/react
just re-exports it so you don't have to worry about this exact thing. Let's
follow suit in @illustration/react
:
import React from 'react';
import {createRoot} from 'react-dom/client';
export {React};
export const hooks = {
'@flecks/web/client.up': async (container, flecks) => {
const results = flecks.invoke('@illustration/react.roots');
// By default `invoke` returns a map: {[fleckPath]: result}
// So, we'll map it into a flat list of components and use the fleck path as the key:
const Components = Object.entries(results)
.map(([fleckPath, Component]) => React.createElement(Component, {key: fleckPath}));
// Finally we'll render all our components as children of the `React.StrictMode` component:
createRoot(container).render(React.createElement(React.StrictMode, {}, Components));
console.log('rendered roots: %O', results);
},
};
Back over in @illustration/component/src/component.jsx
, we'll use it:
import {React} from '@illustration/react';
function Component() {
return <p>Hello world</p>;
}
export default Component;
The page updates automatically: you will see exactly what you expect!
For real this time
Other aspects like HMR will be left as an exercise for the reader. Or... you could just use
@flecks/react
! You really should do that and save yourself the trouble. Back in our application's
build/flecks.yml
let's remove our half-baked React implementation from the build manifest:
'@flecks/core':
id: illustration
'@flecks/server': {}
'@illustration/component': {}
# '@illustration/react': {}
Move into @illustration/component
and add @flecks/react
using the flecks CLI:
- npm
npx flecks add @flecks/react
This is a twofer! It adds the package to dependencies
in package.json
and it also adds
a fleck dependency in flecks.bootstrap.js
:
{
"name": "@illustration/component",
"version": "1.0.0",
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"index.js"
],
"dependencies": {
"@flecks/core": "^4.0.0",
"@flecks/react": "^4.0.0"
},
"devDependencies": {
"@flecks/build": "^4.0.0",
"@flecks/fleck": "^4.0.0"
}
}
exports.dependencies = ['@flecks/react'];
exports.hooks = {};
@illustration/component
has specified all of its dependencies!
It might seem like a bit of busywork to explicitly specify your dependencies because flecks makes it so easy to get along without needing to.
However by doing so you unlock the potential of sharing your code with others which gets at the heart of the flecks way: sharing solutions to repeated problems. Remember: Future you is just as likely to be the beneficiary!
Swap the React
import in @illustration/component/src/component.jsx
:
import {React} from '@flecks/react';
function Component() {
return <p>Hello world</p>;
}
export default Component;
Swap the hook implementation in @illustration/component/src/index.js
:
import Component from './component';
export const hooks = {
'@flecks/react.roots': () => Component,
};
It's what we spent all of this time doing, but better!
The entire reason flecks exists is to minimize duplicated effort.
Enjoy
Now all that's left is to have fun!