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!
Each time an action is dispatched, every connect ed component will be notified, and as a result all of the selectors used by those connect ed components must be re-evaluated. To say that another way: each time an action is dispatched, every selector in the app must be re-evaluated.
Dispatching an action within a reducer is an anti-pattern. Your reducer should be without side effects, simply digesting the action payload and returning a new state object. Adding listeners and dispatching actions within the reducer can lead to chained actions and other side effects.
You can dispatch an action by directly using store. dispatch(). However, it is more likely that you access it with react-Redux helper method called connect(). You can also use bindActionCreators() method to bind many action creators with dispatch function.
You call it from the componentDidMount() of the react component for your page, or wherever it makes sense. So in that component file: import { requestPageOfPlans } from 'actions'; import React from 'react'; import { connect } from 'react-redux'; class MyComponent extends React. Component { componentDidMount() { this.
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.
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.
Maybe the core concept is incorrect and should be refactored?
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
// Assume the store is in store.js with actions already registered
import store from './path/to/store';
import Temperature from './path/to/Temperature';
import Conditions from './path/to/Conditions';
export default Ractive.extend({
components: { Temperature, Conditions },
template: `
<div class="weather">
<!-- pass in state data to presentational components -->
<!-- call methods when events happen from components -->
<Temperature value="{{ temperature }}" on-refresh="refreshTemp()" />
<Conditions value="{{ conditions }}" on-refresh="refreshCond()" />
</div>
`,
data: {
temperature: null,
conditions: null
},
oninit(){
store.subscribe(() => {
// Grab state and set it to component's local state
// Assume the state is an object with temperature and
// conditions properties.
const { temperature, conditions } = store.getState();
this.set({ temperature, conditions });
});
},
// Call actions
refreshTemp(){
store.dispatch({ type: 'TEMPERATURE_REFRESH' });
},
refreshCond(){
store.dispatch({ type: 'CONDITIONS_REFRESH' });
}
});
Temperature.js
// This component is presentational. It is not aware of Redux
// constructs at all. It only knows that it accepts a value and
// should fire refresh.
export default Ractive.extend({
template:`
<div class="temperature">
<span>The temperature is {{ value }}</span>
<button type="button" on-click="refresh">Refresh</button>
</div>
`
});
Conditions.js
// This component is presentational. It is not aware of Redux
// constructs at all. It only knows that it accepts a value and
// should fire refresh.
export default Ractive.extend({
template:`
<div class="conditions">
<img src="http://localhost/condition-images/{{ value }}.jpg">
<button type="button" on-click="refresh">Refresh</button>
</div>
`
});
Where to put initial state hydration from server?
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:
<script>
window.__APP_INITIAL_STATE__ = {...};
</script>
Then when you boot the app, you create a store using that initial state:
import { createStore } from 'redux'
import reducers from './reducers'
let store = createStore(reducers, window.__APP_INITIAL_STATE__);
Should I use one big reducer or by each component one reducer?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With