Search code examples
javascriptreduxreact-reduxractivejs

RactiveJS + Redux dispatch actions and hydrate


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!


Solution

  • 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.