Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why does a reducer needs a default state? #514

Closed
pauldijou opened this issue Aug 14, 2015 · 14 comments
Closed

Why does a reducer needs a default state? #514

pauldijou opened this issue Aug 14, 2015 · 14 comments
Labels

Comments

@pauldijou
Copy link

I might be missing something, but I don't understand why a reducer should have a default state. Most of the doc have something like:

function reducer(state = initialState, action) {...}

I don't see why the reducer should know anything about the default state. The state is managed by the store, the reducer is a pure function that does its job with whatever you give it as argument and not trying to guess what the state is if there is none. But well, I though that it was ok as long as I wasn't forced to put a default state inside the reducer.

Except that combineReducers forces me to put a default state. According to this code, an undefined state will be triggered by Redux to the reducer. Why? My store will pass the default state, so it will never be undefined in practice. My reducer is simple: if it knows the action, it will do something, if not, it will return the state. If the state is undefined, it will return undefined.

Even worse, what if I want to use the same reducer in several different stores with different initial states? Agreed I can always put a fake default state inside the reducer (like 0) and it will be overridden by the store default states, whatever they are, but it feels just wrong.

@gaearon
Copy link
Contributor

gaearon commented Aug 14, 2015

I think you're misunderstanding Redux.

what if I want to use the same reducer in several different stores

There's only a single store in Redux application. You never use more than a single store.

Please read the documentation and the examples, I think this will help clarify the confusion.

https://redux.js.org/basics/reducers

You will see that reducers manage independent parts of the state tree. Therefore they should be able to specify the initial state for these parts.

@gaearon
Copy link
Contributor

gaearon commented Aug 14, 2015

To avoid repeating the documentation here, let's focus on code samples. Please feel free to take code from docs and examples, change it to what you think is a better API, and propose it here.

@pauldijou
Copy link
Author

Wooo... that answer was way too quick. Thanks a lot.

If there is only one store, why is it up to the user to create it? Why is it not created by Redux itself? By providing a createStore, IMO, you actually authorize to have several stores. I find it useful to have two stores in my app, one mostly global and another one for all admin stuff for admin users, just to be sure those data never mix.

I'm fine with the code from the docs, and I'm totally ok with people who want to pass a default state inside the reducer, I just would like to remove the invariant checks inside the combineReducers so that we can also not have a default state inside the reducer if we don't want to. For me, such default state should come from the store. I'm fine with returning undefined if my previous state was undefined.

const INC = 'INC';
const DEC = 'DEC';

function increment() { return { type: INC }; }
function decrement() { return { type: DEC }; }

// Nope (well, it's not wrong but I don't like it)
function reducerAdd(state = 0, action) {
  switch (action.type) {
    case ADD: return state + 1;
    default: return state;
  }
}

function reducerSub(state = 100, action) {
  switch (action.type) {
    case DEC: return state - 1;
    default: return state;
  }
}

const store = createStore(combineReducers({up: reducerAdd, down: reducerSub}));

// Yep !
function reducerAdd(state, action) {
  switch (action.type) {
    case INC: return state + action.value;
    default: return state;
  }
}

function reducerSub(state, action) {
  switch (action.type) {
    case DEC: return state - action.value;
    default: return state;
  }
}

const reducers = combineReducers({up: reducerAdd, down: reducerSub});
const store = createStore(reducers, {up: 0, down: 100});
const anotherStore = createStore(reducers, {up: -1000, down: 1000});

@gaearon
Copy link
Contributor

gaearon commented Aug 14, 2015

I'm fine with the code from the docs, and I'm totally ok with people who want to pass a default state inside the reducer, I just would like to remove the invariant checks inside the combineReducers so that we can also not have a default state inside the reducer if we don't want to. For me, such default state should come from the store. I'm fine with returning undefined if my previous state was undefined.

It was too common source of mistakes. People would forget a default case and would return undefined inadvertently, and then wonder why state is resetting once in a while.

Notice combineReducers is just a helper function. You can trivially right your own without any checks if you don't need them. The default combineReducers that ships with Redux is geared towards helping beginners avoid mistakes and anti-patterns. If you want a vanilla interface, just use createStore(reducer) and create the root reducer anyhow you like. Notice that createStore doesn't have such invariant and doesn't care what reducer returns.

@gaearon
Copy link
Contributor

gaearon commented Aug 14, 2015

The pattern you described doesn't scale.

If may be OK for five-function application, but for a bigger app, every time you're tweaking a reducer, you'll have to go to where its initial state is defined (separate file) and tweak the state's shape there. We want to co-locate reducer logic with its initial state shape so you don't forget to change one when you change another.

@pauldijou
Copy link
Author

Fair enough, it's only a helper, sorry to have bothered you with that and congratz on the 1.0 btw, awesome work.

As for saying it doesn't scale, well, I'm personally fine with the fact that I also have to edit the initial state from the store. If the state architecture is changed, you will probably need to edit several other files anyway, the ones actually reading the state. In my project, I don't want beginners to think that by just tweaking the reducer and its default state, it will magically works in the rest of the app. I also like that by reading only one file, you can see the whole structure of the global state. But it's true that co-locating the logic and the state has its advantages too.

@gaearon
Copy link
Contributor

gaearon commented Aug 14, 2015

I also like that by reading only one file, you can see the whole structure of the global state.

This won't work for “dynamic” reducers anyway. From the async example:

function posts(state = {
  isFetching: false,
  didInvalidate: false,
  items: []
}, action) {
  switch (action.type) {
  case INVALIDATE_REDDIT:
    return Object.assign({}, state, {
      didInvalidate: true
    });
  case REQUEST_POSTS:
    return Object.assign({}, state, {
      isFetching: true,
      didInvalidate: false
    });
  case RECEIVE_POSTS:
    return Object.assign({}, state, {
      isFetching: false,
      didInvalidate: false,
      items: action.posts,
      lastUpdated: action.receivedAt
    });
  default:
    return state;
  }
}

function postsByReddit(state = { }, action) {
  switch (action.type) {
  case INVALIDATE_REDDIT:
  case RECEIVE_POSTS:
  case REQUEST_POSTS:
    return Object.assign({}, state, {
      [action.reddit]: posts(state[action.reddit], action)
    });
  default:
    return state;
  }
}

posts is called dynamically so there's no way for us to specify its shape beforehand. The nice thing about is it's fully self-contained. postsByReddit doesn't need to know what it state shape looks like, and the place where store is defined doesn't even need to know posts reducer exists.

@mindjuice
Copy link
Contributor

A reducer function needs an "accumulator", which in Redux is the state. If there is no accumulator value, then a reducer can't do anything.

In functional programming, for example in Lisp or Clojure, you can reduce on an array of numbers and apply the + function. If you don't provide an accumulator, but there are multiple values in the array, then + can just start by adding the first two values together and it is happy. However, if the array only has a single number, then + would seem to be stuck. The + function is smart though, and knows that a default accumulator of 0 is appropriate for it (just as * knows that 1 is appropriate for it), and so the correct value is returned. Of course, if you pass an accumulator value, then + can also add a single number to that.

Similarly, imagine you have a Redux reducer for ADD_TODO that manages the todos field of your state. If this is the first todo item you've added, and you didn't pass in any state to createStore(), then there would be no todos property in your state. A reasonable thing for the initialState to be in this case might be [].

Since a reducer that handles ADD_TODO is responsible for updating the todos state, it is essential that it knows exactly the shape and nature of the state it is managing. Of course, this can be delegated to sub-reducers too when composing reducers, in which case the higher level reducer need not know the details of the sub-reducers.

default state should come from the store

Maybe this is just awkward phrasing, but that's not quite right. The store is a dumb container. It just holds onto the state. The store doesn't know anything about your app. It gets state either when you create it (optional) or as returned from reducers.

Regarding keeping separate stores for normal data and admin data, you will get the exact same level of safety/separation by creating and composing separate reducer functions. The reducer function for your global state can't accidentally change the admin state, since it has absolutely no way to see it. Each reducer is given only the subset of the data for which it is responsible.

I understand that in your case, you want to pass in the initial state, however, unless you are actually populating that state with non-default values stored (e.g., data from a database or in a web browser's local storage), I would suggest that you are better off just putting the defaults in each reducer, since it's necessary anyway.

BTW, I wrote this as much for myself to confirm my understanding, as to try to answer your question, so anyone please let me know if I am mistaken anywhere. 😄

@gaearon
Copy link
Contributor

gaearon commented Aug 14, 2015

👍

@pauldijou
Copy link
Author

Gotcha, thanks a lot for all the explanations guys! I will rethink how I am currently using Redux.

@jimmyn
Copy link

jimmyn commented Sep 19, 2016

@gaearon is it worth writing

const intitialState = [];
const ids = (state = initialState, action) => {...}

instead of simply const ids = (state = [], action) => {...} because of [] !== [] to reduce rerenders? And the same with objects.

@markerikson
Copy link
Contributor

@jimmyn : that's semantically the same. The key issue is whether the incoming state argument has a value of undefined, in which case ES6's default arguments feature kicks in.

See Dan's explanation of how reducer default arguments and Redux createStore preloaded state interact, at http://stackoverflow.com/questions/33749759/read-stores-initial-state-in-redux-reducer/33791942 .

@Blunderchips
Copy link

I think you're misunderstanding Redux.

what if I want to use the same reducer in several different stores

There's only a single store in Redux application. You never use more than a single store.

Please read the documentation and the examples, I think this will help clarify the confusion.

http://rackt.github.io/redux/docs/basics/Reducers.html

You will see that reducers manage independent parts of the state tree. Therefore they should be able to specify the initial state for these parts.

Link is dead

@markerikson
Copy link
Contributor

Updated to point to the current Redux docs page at https://redux.js.org/basics/reducers .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants