Do I have to return state in a Redux Reducer?

Viewed 1561

I am having trouble wrapping my head around the way a redux-reducer works in JavaScript.

Here is the example I am working with:

const intitialState = {
  profile: null,
  profiles: [],
  repos: [],
  loading: true,
  error: {},
};

export default function (state = intitialState, action) {
  const { type, payload } = action;

  switch (type) {
    case GET_PROFILE:
      return {
        ...state,
        profile: payload,
        loading: false,
      };
    case PROFILE_ERROR:
      return {
        ...state,
        error: payload,
        loading: false,
      };
    default:
      return state;
  }
}

Which of these assumptions is correct, if either, about this reducer?

A) When GET_PROFILE is triggered, it returns '...state' as the initial state so that the redux dev tools can compare states.

B) When GET_PROFILE is triggered, it is really just filling in the values into the object that I didn't explicitly list. As in the return really looks like this:

return {
        profiles: profiles,
        repos: repos,
        error: error,
        profile: payload,
        loading: false,
      };

And if A or B is true, then what happens if I do not return ...state as the first one, will this mess up the other objects?

Edit: And where exactly does it return to?

3 Answers

I would say B is true.

If you do not return the state, the data in that unreturned state will be lost.


The function createStore gives you a store object with 5 core functions.

I would argue the following 3 functions return from the store the most notable:

  • subscribe function - used for subscribing to the store. This function returns an unsubscribe function for unsubscribing.
    • If you are using react-redux, this subscribe/unsubscribe logic is already handled under the hood with the connect function or the useSelector hook.
  • getState function - used for getting the current state.
    • If you are using react-redux This is also handled inside the connect function or the useSelector hook.
  • dispatch function - used for making updates to the global state.
    • If you are using react-redux This is handled inside the connect function or the useDispatch hook.

Not returning a copy of the state from your reducer will cause you to lose the state you did not return. To understand why, it is helpful to look at the store's dispatch function.


Inside of the store's dispatch function, you will see that a currentState variable is being reassigned to whatever is being returned from the reducer (or combined reducer).

The getState function from the store simply returns this currentState variable. The current state is always retrieved from this getState function.


So when you dispatch an action, your state tree is going to be updated with whatever you return from your reducers.


Here is a small vanilla example of what happens when you return state vs. when you return no state from a redux reducer.

You can run and rerun the snippet. The blue button dispatches GET_PROFILE with a copy of the returned state. The yellow button dispatches an action GET_PROFILE_NO_STATE_COPY_RETURNED that returns the same object as GET_PROFILE without the state copy ...state.

Actually, once you click on GET_PROFILE_NO_STATE_COPY_RETURNED, you will need to rerun the snippet again, because the state properties not specified are completely gone :)

The redux state is updated with whatever is returned from the reducer.

I used the reducer from your question:

const createStore = (reducer, preloadedState, enhancer) => {
  if (typeof enhancer !== 'undefined') {
    return enhancer(createStore)(reducer, preloadedState);
  }
  let currentState = preloadedState;
  let listeners = [];

  const getState = () => currentState;

  const subscribe = listener => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  };

  // The returned from the reducer
  // inside the dispatch function 
  // will be the new state.
  // https://github.com/reduxjs/redux/blob/master/src/createStore.ts#L246
  const dispatch = action => {
    currentState = reducer(currentState, action);
    listeners.forEach(l => l());
  };

  // initialize state
  dispatch({});

  return {
    getState,
    subscribe,
    dispatch,
  };
};

const initialState = {
  profile: null,
  profiles: [],
  repos: [],
  loading: true,
  error: {},
};

// action type constants
const GET_PROFILE = 'GET_PROFILE';
const PROFILE_ERROR = 'PROFILE_ERROR';
const GET_PROFILE_NO_STATE_COPY_RETURNED = 'GET_PROFILE_NO_STATE_COPY_RETURNED';

// reducer
const reducer = (state = initialState, {
  type,
  payload
}) => {
  switch (type) {
    case GET_PROFILE:
      return {
        ...state,
        profile: payload,
        loading: false,
      };
    case GET_PROFILE_NO_STATE_COPY_RETURNED:
      return {
        profile: payload,
        loading: false,
      };
    case PROFILE_ERROR:
      return {
        ...state,
        error: payload,
        loading: false,
      };
    default:
      return state;
  }
};

const store = createStore(reducer);

const render = () => {
  const html = `
    <p>State</p>
    <pre><code>${JSON.stringify(store.getState(), null, 4)}</code></pre>
`;
  document.getElementById('app').innerHTML = html;
};

store.subscribe(render);

render();

document.getElementById('get-profile').addEventListener('click', () => {
  const firstName = document.getElementById('firstName').value;
  const lastName = document.getElementById('lastName').value;
  store.dispatch({
    type: GET_PROFILE,
    payload: {
      firstName,
      lastName,
    },
  });
});

document
  .getElementById('get-profile-no-state-returned')
  .addEventListener('click', () => {
    const firstName = document.getElementById('firstName').value;
    const lastName = document.getElementById('lastName').value;
    store.dispatch({
      type: GET_PROFILE_NO_STATE_COPY_RETURNED,
      payload: {
        firstName,
        lastName,
      },
    });
  });
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet" />

<div id="app"></div>

<input class="form-control" id="firstName" placeholder="firstName" />
<input class="form-control" id="lastName" placeholder="lastName" />
<button class="btn btn-primary" id="get-profile">GET_PROFILE </button>
<button class="btn btn-warning" id="get-profile-no-state-returned">GET_PROFILE_NO_STATE_COPY_RETURNED</button>


So when you dispatch an action, the returned value from the reducer is going to be your new state.

[B] would be the most correct answer but it is not completely accurate.

The state is replace at each event (or action type). The initialState is just relevant for the first step.

As you can see in the following exemple, when ADD_RECIPIENT is triggered, the line:

recipients: [...state.recipients, recipient]

means that state.recipients is the concatenation of itself plus the new "recipient" to add. It is the same process with:

return {
    ...state,
    recipients: [...state.recipients, recipient]
}

state is replaced by the concatenation of itself.


Exemple:

const initialState = {
  selectedRecipient: '',
  recipients: []
};

export default function (state = initialState, action) {
  switch (action.type) {
      case LOAD_RECIPIENTS: {
          const { recipients } = action.payload;
          return {
              ...state,
              selectedRecipient: recipients[0],
              recipients: recipients
          };
      }
      case ADD_RECIPIENT: {
          const { recipient } = action.payload;
          return {
              ...state,
              recipients: [...state.recipients, recipient]
          };
      }
...
...

It works like B option. The state will be updated with new values (spread operator is being used). Then, the new state is sent to react (return will work that way) react checks if anything has been changed and updates the DOM if needed. Very simplified model but close to real.

Related