Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redux - Loading initial state asynchronously

I'm trying to work out the cleanest way to load the initial state of my Redux stores when it comes from API calls.

I understand that the typical way of providing the initial state is to generate it server-side on page load, and provide it to Redux createStore() as a simple object. However, I'm writing an app that I'm planning on packaging up in Electron and so this doesn't work.

The best that I've been able to come up with so far is to fire an action immediately after creating the store that will go and request the initial state for the store - either one action that retrieves the entire initial state or a number of actions that each retrieve the initial state for one part of the store. This would then mean that my code looks like:

const store = createStore(reducer, Immutable.Map(), middleware);
store.dispatch(loadStateForA());
store.dispatch(loadStateForB());
store.dispatch(loadStateForC());

Whilst this will work, it seems a bit on the crude side and so I'm wondering if there's some better alternative that I'm missing?

like image 979
Graham Avatar asked May 23 '16 14:05

Graham


People also ask

Are Redux state update synchronously?

Introduction. By default, Redux's actions are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects. Redux also allows for middleware that sits between an action being dispatched and the action reaching the reducers.

How do you set initial state in Redux?

There are two main ways to initialize state for your application. The createStore method can accept an optional preloadedState value as its second argument. Reducers can also specify an initial value by looking for an incoming state argument that is undefined , and returning the value they'd like to use as a default.

Is Redux state async?

As it turns out, Redux already has an official version of that "async function middleware", called the Redux "Thunk" middleware. The thunk middleware allows us to write functions that get dispatch and getState as arguments.

Do reducers need initial state compulsorily?

In a nutshell: it's Redux the one who passes the initial state to the reducers, you don't need to do anything.


Video Answer


3 Answers

I also encountered the same problem (also building an electron app). A part of my store has application settings which gets persisted on local file system and I needed to load it asynchronously on application's startup.

This is what I come up with. Being a "newbie" with React/Redux, I am very much interested in knowing the thoughts of the community on my approach and how it can be improved.

I created a method which loads the store asynchronously. This method returns a Promise which contains the store object.

export const configureStoreAsync = () => {
  return new Promise((resolve) => {
    const initialState = initialStoreState;//default initial store state
    try {
        //do some async stuff here to manipulate initial state...like read from local disk etc. 
        //This is again wrapped in its own Promises.
        const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
        resolve(store);
      });
    } catch (error) {
      //To do .... log error!
      const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
      console.log(store.getState());
      resolve(store);
    }
  });
};

Then in my application entry point, here's how I used it:

configureStoreAsync().then(result => {
  const store = result;
  return ReactDOM.render(
    <Provider store={store}>
      <App store={store}/>
    </Provider>,
    document.getElementById('Main'));
});

Like I said, this is my naive attempt at solving this problem and I am sure there must be better ways of handling this problem. I would be very much interested in knowing how this can be improved.

like image 97
Gaurav Mantri Avatar answered Oct 27 '22 09:10

Gaurav Mantri


As far as I can tell, you have only two options (logically):

  1. Set the initial state after the store is instantiated
  2. Set the initial state when the store is instantiated

Option 1 must be done using an action:

The only way to change the state is to emit an action, an object describing what happened.

— One of "Three Principles" in the docs

This is what you've tried, but you think it is crude for some reason.


The alternative is just to call createStore after your asynch request has resolved. One solution has already been posted (by @Gaurav Mantri) using a Promise object, which is a nice approach.

I would recommend against this, since you will likely have multiple modules trying to require or import your store (or store.dispatch, or store.subscribe) before it exists; they would all have to be made to expect Promises. The first method is the most Redux-y.

like image 27
A. Vidor Avatar answered Oct 27 '22 09:10

A. Vidor


My app startup workflow:

  1. Loading spinner in index.html
  2. Ajax to check if user is logged in
  3. On ajax end, render the Root component
  4. Hide the loading spinner

I achieved that by:

  1. Creating the store with a custom middleware that listens for the initial ajax end action and calls a callback once
  2. Dispatching the initial ajax action

root.js

const store = createStore(
    rootReducer,
    applyMiddleware(
        ...,
        actionCallbackOnceMiddleware(INITIAL_AJAX_END, render)
    )
)

function render() {
    ReactDOM.render(
        <Provider store={store}>
            <RootComponent/>
        </Provider>,
        document.getElementById('root')
    )

    document.getElementById('loading').dispatchEvent(new Event('hide'))
}

store.dispatch(initialAjaxAction());

middleware/actionCallbackOnce.js

export default (actionType, callback) => store => next => {
    let called = false;

    return action => {
        next(action);

        if (!called && action.type === actionType) {
            called = true;
            callback();
        }
    }
}

index.html

<div id="loading">
    <span>Loading</span>
    <style type="text/css">...</style>
    <script>
        (function(loading){
            loading.addEventListener('hide', function(){
                loading.remove();
            });
            loading.addEventListener('error', function(){
                loading.querySelector('span').textContent = "Error";
            });
        })(document.getElementById('loading'));
    </script>
</div>

<div id="root"></div>
like image 32
Oleg Avatar answered Oct 27 '22 10:10

Oleg