re-structure v0.2.3
Re-structure
A pattern for structuring React applications. Heavily based on the re-frame ClojureScript project.
The re-frame README pretty much says it all. This is an approximate adaptation of that idea, meant for building JavaScript applications.
Examples
See here.
Run the examples locally:
npm install
npm run examplesSimple time/color example from reagent
: http://localhost:8000/simple.bundle
TodoMVC example : http://localhost:8000/todomvc.bundle
Application "pages" example : http://localhost:8000/pages.bundle
Concepts
Application DB
Just like re-frame has one global application db structure that holds the entire application state, re-structure does too. Your application db is just a data stucture that you create and initialize yourself. You can use Immutable.js, which makes rendering more performant, but it's not required; you could use a regular JS object or any other data structure you want.
Once you initialize your db you have to register it with re-structure when bootstrapping your app:
import {initApp} from 're-structure';
// initialize the application db
let db = Immutable.fromJS({
timer: new Date(),
timeColor: '#f34'
});
// register application db with re-structure
initApp(db);
// render your app
React.render(<App />, document.body);initApp accepts an optional second parameter which defines debugging options. If set to true, re-structure will log all db inputs/commands and db outputs/projections to the developer console.
For more control of what is logged, provide a configuration object of the format: {logCommands: true, logProjections: false}. To log a subset of the application db, provide a projection function as the logProjections value:
initApp(db, {logProjections: db => db.get('timer')});In the example above, the timer value will be logged to the console when it changes.
Projections
Projections are very similar to subs in re-frame. Where in re-frame you would do this:
(register-sub
:time-color
(fn
[db _]
(reaction (:time-color @db))))In re-structure you do this:
// project a part of the application db
export function timeColor(db) {
return db.get('timeColor');
}This defines a normal function that takes the current state of the application db as a parameter and returns a "projected" view of it, exactly what the registered sub does in the re-frame example. Note that you didn't have to do anything special like register the function or wrap the result in a reaction.
Views
An example View:
import {timer} from './projections';
// a view is just a decorated React component
export default @View class Clock extends React.Component {
// declare projections that the view depends on
static projections = {timer};
render() {
{/* the current value of the projection is available as a prop */}
return <div>{this.props.timer}</div>;
}
}Views have one ability that distinguishes them from normal React components: they can render projection values reactively as they change. When a View depends on a projection's results, it declares so in the static projections property. Then, whenever the projection has an updated value (according to ===), the View is automatically re-rendered and the new value is available as a prop with the same name (this.props.timer in the example above). This is along the same lines as how reactions are used in re-frame/reagent.
Beacuse Views are just decorated React components, you can still keep local state, use lifecycle methods, etc.
Projections can also be parameterized with props values:
static projections = props => ({
page: db => db.getIn(['pages', props.pageNumber])
});In this example, projections defines a function that takes props as a parameter and defines a page projection which projects a part of the application state (located using the props.pageNumber). Note that the projections function returns an object that looks like a normal projections definition.
Here's how we might declare a projection inline that doesn't need props:
static projections = {
pageOne: db => db.getIn(['pages', '1'])
};Commands
A command is a normal function that takes the current application db value and optional parameters and returns a new version of the application db, with changes applied to it. It is similar to an event handler in re-frame.
An example command:
// return a new version of db with an updated timeColor value
export function setTimeColor(db, {color}) {
return db.set('timeColor', color);
}To emit a command, you do just that:
import {setTimeColor} from './commands';
let ColorInput = @View class ColorInput extends React.Component {
static projections = {timeColor};
render() {
return (
<div className="color-input">
Time Color: <input type="text"
value={this.props.timeColor}
onChange={e => emit(setTimeColor, {color: e.target.value})}/>
</div>
);
}
}