I am trying to set up RactiveJS with Redux for small example application - initialize dashboard (from AJAX), add/remove elements (widgets) from dashboard (and save serialized data on server). As there are tutorials almost exclusively for React, then I need advice. I followed some and got directory structure like:
views
app.html
dashboard.html
widget.html
js
actions
DashboardActions.js
components
Dashboard.js
Widget.js
constants
ActionTypes.js
reducers
dashboard.js
index.js
app.js
index.html
This example works, but there are several problems and I would like to figure out how to make it better. For example:
1) How to pass (and should I pass?) store and actions down to Ractive component tree? At now it uses bindActionCreators
in each component and I think this is not good solution.
2) Where to put initial state hydration from server? At now it is hardcoded in reducers/dashboard.js
, but I would like to use backend as data source and data save endpoint. There is middleware approach, but if this is good practice, then how to apply that with RactiveJs?
3) Should I use one big reducer
or by each component one reducer
?
4) Maybe the core concept is incorrect and should be refactored?
views/app.html
<Dashboard dashboard={{store.getState()}} store="{{store}}"></Dashboard>
views/dashboard.html
{{#with dashboard}}
<pre>
====
<a on-click="@this.addWidget('Added by click')" href="#">Add New</a>
{{#dashboard}}
{{#each widgets}}
<Widget id="{{this.id}}" name="{{this.name}}" size="{{this.size}}" actions="{{actions}}" store="{{store}}"></Widget>
{{/each}}
{{/dashboard}}
====
</pre>
{{/with}}
views/widget.html
<div>{{id}}-{{name}} (Size: {{size}})<a href="#" on-click="@this.deleteWidget(id)">X</a></div>
actions/DashboardActions.js
import * as types from '../constants/ActionTypes';
// Add widget to dashboard
export function addWidget(name) {
return {
type: types.ADD_WIDGET,
name
};
}
// Delete widget from dashboard
export function deleteWidget(id) {
return {
type: types.DELETE_WIDGET,
id
};
}
components/Dashboard.js
import Ractive from 'ractive'
import * as DashboardActions from '../actions/DashboardActions';
import { dispatch, bindActionCreators } from 'redux'
import Widget from './Widget'
import template from '../../views/dashboard.html';
export default Ractive.extend({
isolated: true,
components: {
Widget
},
oninit() {
const store = this.get("store");
const actions = bindActionCreators(DashboardActions, store.dispatch);
this.set("actions", actions);
},
addWidget(name) {
this.get("actions").addWidget(name);
},
template: template
});
components/Widget.js
import Ractive from 'ractive'
import * as DashboardActions from '../actions/DashboardActions';
import { dispatch, bindActionCreators } from 'redux'
import template from '../../views/widget.html';
export default Ractive.extend({
isolated: true,
template: template,
oninit() {
console.log(this.get("actions"));
const store = this.get("store");
const actions = bindActionCreators(DashboardActions, store.dispatch);
this.set("actions", actions);
},
deleteWidget(id) {
this.get("actions").deleteWidget(id);
},
})
constants/ActionTypes.js
// Add widget to dashboard
export const ADD_WIDGET = 'ADD_WIDGET';
// Delete widget from dashboard
export const DELETE_WIDGET = 'DELETE_WIDGET';
reducers/dashboard.js
import * as types from '../constants/ActionTypes';
const initialState = {
widgets: [
{id: 1, name: "First widget"},
{id: 2, name: "Second widget"},
{id: 3, name: "Third widget"},
],
};
export default function dashboard(state = initialState, action) {
switch (action.type) {
case types.ADD_WIDGET:
const newId = state.widgets.length + 1;
const addedWidgets = [].concat(state.widgets, {
id: newId,
name: action.name
});
return {
widgets: addedWidgets
}
case types.DELETE_WIDGET:
const newWidgets = state.widgets.filter(function(obj) {
return obj.id != action.id
});
return {
widgets: newWidgets
}
default:
return state;
}
}
reducers/index.js
export { default as dashboard } from './dashboard';
app.js
import Ractive from 'ractive';
import template from '../views/app.html';
import Dashboard from './components/Dashboard.js'
import { createStore, combineReducers, bindActionCreators } from 'redux'
import * as reducers from './reducers'
const reducer = combineReducers(reducers);
const store = createStore(reducer);
let App = new Ractive({
el: '#app',
template: template,
components: {
Dashboard
},
data: {
store
}
});
store.subscribe(() => App.update());
export default App;
Thanks!
Ractive doesn't impose any convention as to how this is done. However, Ractive is designed similar to other frameworks (lifecycle hooks, methods, etc.). So what works for you on other frameworks should also just work in Ractive.
I'm pretty sure you're confused whether to assign stores and actions directly to components or pass them down via ancestors. The answer is... both. The author of Redux actually splits components into 2 kinds: presentational and containers.
In a gist, container components hold state and call actions. Presentational components are stateless and receive stuff from ancestor components.
Say you have a weather widget that shows temperature and conditions. You would have 3 components, the widget component itself, temperature, and conditions. Both temperature and conditions components are presentational. The weather component will be the container that grabs the data, hands them over to both components, as well as transform UI interaction into actions.
Weather.js
Temperature.js
Conditions.js
If I remember correctly, one isomorphic workflow I saw involved putting the server-provided state in a carefully-named global variable. On application start, the app picks up the data in that global and feeds it into the store. Ractive is not involved in this process.
This will be printed by your server on the page:
Then when you boot the app, you create a store using that initial state:
Redux has a good guide on how to split up reducers as well as how to normalize state shape. In general, state shape isn't defined by component but more by functionality.