Redux: Opinions/examples of how to do backend persistence?
Asked Answered
R

2

49

I am wondering how folks using Redux are approaching their backend persistence. Particularly, are you storing the "actions" in a database or are you only storing the last known state of the application?

If you are storing the actions, are you simply requesting them from the server, then replaying all of them when a given page loads? Couldn't this lead to some performance issues with a large scale app where there are lots of actions?

If you are storing just the "current state", how are you actually persisting this state at any given time as actions happen on a client?

Does anyone have some code examples of how they are connecting the redux reducers to backend storage apis?

I know this is a very "it depends on your app" type question, but I'm just pondering some ideas here and trying to get a feel for how this sort of "stateless" architecture could work in a full-stack sense.

Thanks everyone.

Reincarnate answered 5/10, 2015 at 13:28 Comment(1)
I think the backend is usually pretty classical (a normal DB), not different from other CRUD apps. However you might be interested in approaches like rethinkdb.com and confluent.io/blog/….Perky
S
36

Definitely persist the state of your reducers!

If you persisted a sequence of actions instead, you wouldn't ever be able to modify your actions in your frontend without fiddling around inside your prod database.

Example: persist one reducer's state to a server

We'll start with three extra action types:

// actions: 'SAVE', 'SAVE_SUCCESS', 'SAVE_ERROR'

I use redux-thunk to do async server calls: it means that one action creator function can dispatch extra actions and inspect the current state.

The save action creator dispatches one action immediately (so that you can show a spinner, or disable a 'save' button in your UI). It then dispatches SAVE_SUCCESS or a SAVE_ERROR actions once the POST request has finished.

var actionCreators = {
  save: () => {
    return (dispatch, getState) => {
      var currentState = getState();
      var interestingBits = extractInterestingBitsFromState(currentState);

      dispatch({type: 'SAVE'});

      window.fetch(someUrl, {
        method: 'POST',
        body: JSON.stringify(interestingBits)
      })
      .then(checkStatus) // from https://github.com/github/fetch#handling-http-error-statuses
      .then((response) => response.json())
      .then((json) => dispatch actionCreators.saveSuccess(json.someResponseValue))
      .catch((error) =>
        console.error(error)
        dispatch actionCreators.saveError(error)
      );
    }
  },

  saveSuccess: (someResponseValue) => return {type: 'SAVE_SUCCESS', someResponseValue},

  saveError: (error) => return {type: 'SAVE_ERROR', error},

  // other real actions here
};

(N.B. $.ajax would totally work in place of the window.fetch stuff, I just prefer not to load the whole of jQuery for one function!)

The reducer just keeps track of any outstanding server request.

function reducer(state, action) {
  switch (action.type) {
    case 'SAVE':
      return Object.assign {}, state, {savePending: true, saveSucceeded: null, saveError: null}
      break;
    case 'SAVE_SUCCESS':
      return Object.assign {}, state, {savePending: false, saveSucceeded: true, saveError: false}
      break;
    case 'SAVE_ERROR': 
      return Object.assign {}, state, {savePending: false, saveSucceeded: false, saveError: true}
      break;

    // real actions handled here
  }
}

You'll probably want to do something with the someResponseValue that came back from the server - maybe it's an id of a newly created entity etc etc.

I hope this helps, it's worked nicely so far for me!

Stodgy answered 10/10, 2015 at 14:37 Comment(2)
Thanks for the input Dan! This seems to work nicely!Reincarnate
This is great help. A side note - if you store both the reduced state AND the actions, you can then use event-driven design techniques to allow server-side time travel or versioning, of a kind. Useful for some things!Samuella
K
2

Definitely persist the actions!

This is only a counterexample, adding to Dan Fitch's comment in the previous answer.

If you persisted your state, you wouldn't ever be able to modify your state without altering columns and tables in your database. The state shows you only how things are now, you can't rebuild a previous state, and you won't know which facts had happened.

Example: persist an action to a server

Your action already is a "type" and a "payload", and that's probably all you need in an Event-Driven/Event-Sourcing architecture.

You can call your back-end and send the actions inside your actionCreator (see Dan Fox's answer).

Another alternative is to use a middleware to filter what actions you need to persist, and send them to your backend, and, optionally, dispatch new events to your store.

const persistenceActionTypes = ['CREATE_ORDER', 'UPDATE_PROFILE'];
// notPersistenceActionTypes = ['ADD_ITEM_TO_CART', 'REMOVE_ITEM_FROM_CART', 'NAVIGATE']

const persistenceMiddleware = store => dispatch => action => {
  const result = dispatch(action);
  if (persistenceActionTypes.indexOf(action.type) > -1) {
  // or maybe you could filter by the payload. Ex:
  // if (action.timestamp) {
      sendToBackend(store, action);
  }
  return result;
}

const sendToBackend = (store, action) => {
  const interestingBits = extractInterestingBitsFromAction(action);
  // déjà vu
  window.fetch(someUrl, {
    method: 'POST',
    body: JSON.stringify(interestingBits)
  })
  .then(checkStatus)
  .then(response => response.json())
  .then(json => {
    store.dispatch(actionCreators.saveSuccess(json.someResponseValue));
  })
  .catch(error => {
    console.error(error)
    store.dispatch(actionCreators.saveError(error))
  });
}
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';

createStore(
  yourReducer,
  aPreloadedState,
  applyMiddleware(thunk, persistenceMiddleware)
)

(You can also use a middleware to send current state to the backed. Call store.getState().)

Your app already knows how to transform actions into state with reducers, so you can also fetch actions from your backend too.

Kentish answered 16/7, 2019 at 14:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.