Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to update related state fields with split reducers?

Tags:

redux

reducers

I'm trying to work out the ideal way to update several top level fields on my state tree while still maintaining split reducers.

Here's a simple solution that I've come up with.

var state = {
  fileOrder: [0],
  files: {
    0:{
      id: 0,
      name: 'asdf'
    }
  }
};

function handleAddFile(state, action) {
  return {...state, ...{[action.id]:{id: action.id, name: action.name}}};
};

function addFileOrder(state, action) {
  return [...state, action.id];
}

// Adding a file should create a new file, and add its id to the fileOrder array.
function addFile(state, action) {
  let id = Math.max.apply(this, Object.keys(state.files)) + 1;
  return {
    ...state,
    fileOrder: addFileOrder(state.fileOrder, {id}),
    files: handleAddFile(state.files, {id, name: action.name})
  };
}

Currently I'm able to dispatch a single action {type: ADD_FILE, fileName: 'x'}, then addFile creates an action internally to send to addFileOrder and addFile.

I'm curious if it is considered a better approach to do either of the below.

Instead dispatch two actions, one to add a file, then get it's id and dispatch an ADD_TO_FILE_ORDER action with the id. OR Fire and action like {type: ADD_FILE, name: 'x', id: 1}, instead of allowing addFile to calculate the new id. This would allow me to use combineReducers and filter on action type. This example is probably trivial, but my actual state tree is a bit more complicated, with each file being added also needing to be added to other entities.

For some additional context, a more complete state tree would look like this.

{
    "fileOrder": [0]
    "entities": {
        "files": {
            0: {
                id: 0,
                name: 'hand.png'
            }
        },
        "animations": {
            0: {
                id: 0,
                name: "Base",
                frames: [0]
            }
        },
        "frames": {
            0: {
                id: 0,
                duration: 500,
                fileFrames: [0]
            }
        },
        "fileFrames": {
            0: {
                id: 0,
                file: 0,
                top: 0,
                left: 0,
                visible: true
            }           
        }
    }
}

Adding a file would need to:

  1. Add it to the files hash.
  2. Add it to the fileOrder array.
  3. Add a fileFrame referencing the file, for each of the frames.
  4. Add each new fileFrame to the frame that it was created for.

The last two points make me wonder if I'd be able to use combineReducers at all.

like image 992
Gary Avatar asked Oct 24 '15 17:10

Gary


People also ask

How can I update my state reducer?

Updating State Using Reducers: The Spread Operator State can't be directly changed, to create or update state, we can use the JavaScript spread operator to make sure we don't change the value of the state directly but instead to return a new object that contains a state passed to it and the payload of the user.

Can reducers share state?

The reducer must always produce the same state given the same current state and action, it must be a pure function. A common pattern is to compose multiple reducers that act on separate parts of the state, i.e. properties of the state object.

How do you pass initial state to reducer?

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.


2 Answers

I ended up finding a pretty simple solution to this problem.

Both of these blocks from the documentation are functionally the same thing.

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
});

// This is functionally equivalent.
function reducer(state, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  };
}

I ended up tweaking the second block, and always passing along my global state tree. As long as nothing edits the state along the way, all the reducers work fine.

// Simple change to pass the entire state to each reducer.
// You have to be extra careful to keep state immutable here.
function reducer(state, action) {
  return {
    a: doSomethingWithA(state.a, action, state),
    b: processB(state.b, action, state),
    c: c(state.c, action, state)
  };
}
like image 120
Gary Avatar answered Oct 05 '22 13:10

Gary


Building on the author's solution:

I've been having this same issue, where I need (just a little) access outside of my reducer's part of of the state. I think this solution can work in practice if you're diligent about not changing anything other than a single value like a flag, or a counter.

It's impurity could get crazy fast if other developers weren't as reserved with their code. Imagine what would happen if a started changing b and c's part of the state, b changing a and c's part, and so on.

You might consider shrinking the surface area of impurity like this:

function reducer(state, action) {
  return {
    a: doSomethingWithA(state.a, action, state.globals),
    b: processB(state.b, action, state.globals),
    c: c(state.c, action, state.globals)
  };
}
like image 24
ModernIncantations Avatar answered Oct 05 '22 14:10

ModernIncantations