How to put methods onto the objects in Redux state?

26,581

Solution 1

In Redux, you don't really have custom models. Your state should be plain objects (or Immutable records). They are not expected to have any custom methods.

Instead of putting methods onto the models (e.g. TodoItem.rename) you are expected to write reducers that handle actions. That's the whole point of Redux.

// Manages single todo item
function todoItem(state, action) {
  switch (action.type) {
    case 'ADD':
      return { name: action.name, complete: false };

    case 'RENAME':
      return { ...state, name: action.name };

    case 'TOGGLE_COMPLETE':
      return { ...state, complete: !state.complete };

    default:
      return state;
  }
}

// Manages a list of todo items
function todoItems(state = [], action) {
  switch (action.type) {
    case 'ADD':
      return [...state, todoItem(undefined, action)];

    case 'REMOVE':
      return [
        ...state.slice(0, action.index),
        ...state.slice(action.index + 1)
      ];

    case 'RENAME':
    case 'TOGGLE_COMPLETE':
      return [
        ...state.slice(0, action.index),
        todoItem(state[action.index], action),
        ...state.slice(action.index + 1)
      ];
  }
}

If this still doesn't make sense please read through the Redux basics tutorial because you seem to have a wrong idea about how Redux applications are structured.

Solution 2

Dan's answer is of course correct in terms of how you should use redux when writing an application from scratch. My answer is based on a non-ideal motivating situation that may be similar to the OPs situation and how I am thinking of dealing with it.

I find myself trying create a redux application that depends on 3rd party libraries within the redux reducer that perform operations on input objects with methods, and I wanted to keep these input objects in the redux state. Here is my motivating situation in pseudo-code, and how I am "solving" it using lodash's assign method:

// Library A is a large 3rd party library I don't want to change
// Library A expects instances of MyClass to have a method 'complexOperation', and also to have property 'a'
LibraryA = class {
  static reliesOnComplexOperation(myClassInstance) {
    if (myClassInstance.complexOperation() && myClassInstance.a < 100) {
      return true;
    } else {
      return false;
    }
  }
}

// MyClass is my own class that I control, and I want to store it's state and make changes to it using the redux reducer.
MyClass = class {
  constructor() {
    this.a = 0;
  }
  myComplexOperation() {
    return a > 10;
  }
}

//and here is my redux reducer, making use of Library A
function reducer(state, action) {
  switch (action.type) {
    case CHANGE_MY_CLASS:
      const myClassInstancePlain = state.myClassInstance;
      // redux has converted myClassInstance into a plain object with no methods, so we need to create a new instance of MyClass, and assign property values from the plain object to the new instance. Using the assign function from lodash library to achieve this - likely not the most efficient way
      const myClassInstance = _.assign(new MyClass(), myClassInstancePlain);

      // now I can pass myClassInstance off to LibraryA, and the complexOperation method will be available
      if (LibraryA.reliesOnComplexOperation(myClassInstance)) {
        myClassInstance.a = 50;
      }
      // I can return myClassInstance as part of the new state, and redux will convert it to a plain object
      return {
        ...state,
        myClassInstance
      };
    default:
      return state;
  }
}

So this example shows one way of incorporating objects with methods in the redux state as plain objects, and then adding back the methods so they can be used within the redux reducer. Would love to hear Dan's or anyone else's thoughts on this pattern or how it could be done better. I've seen Dan post in several places that storing objects with methods is a bad idea in redux, so the goal here is to find a way to store the object as a plain object and attach the methods when you need them in an efficient way.

Solution 3

I'm not sure this is exactly relevant (the context is using Redux with React), but I found this page while searching for something similar, so I'm writing this in case it is of interest.

My motivation is, I have have state objects where I would like to expose a representation of that state rather than the state itself. So for example (using Immutable.js), I have a state (a schema) that comprises one or more fields, each identified by a unique identifier and stored in a Map, plus a List of identifiers that gives the field ordering.

I really don't want to writing stuff like

state.get('field').get(state.get('order').get(nth))

to get the nth field anywhere other than close to the reducer, so that the internal representation is hidden and can be changed easily.

What I'm currently doing (and this is a bit of an experiment), is to add a function in the same file as the reducer:

schema.mapVisibleState = function (state) {
    return {
        getOrderedFields: () => state.get('order').map((fid) => state.get('fields').get(fid)
    }
}

This is used in the corresponding React components (along with a similarly motivated schema.bindActionCreators function):

export const Schema = connect(
    (state) => schema.mapVisibleState (state),
    (dispatch) => schema.bindActionCreators (dispatch)
)(_Schema) ;

Now I can access the fields, in the correct order, inside the component simply as this.props.getOrderedFields() and not worry about the underlying representation. It also has the additional nice property that it is easy to do dependency injection of the getOrderedFields() function.

Also, since I have a similar structure for the field state, I can extend this:

schema.mapVisibleState = function (state) {
    return {
        getOrderedFields: () => state.get('order').map((fid) => field.mapVisibleState(state.get('fields').get(fid)),
        getNthField (nth) => field.mapVisibleState(state.get('fields').get(state.get('order').get(nth)))
    }
}
Share:
26,581
Yan
Author by

Yan

Updated on November 20, 2020

Comments

  • Yan
    Yan over 3 years

    According to docs state of react app has to be something serializable. What about classes then?

    Let's say I have a ToDo app. Each of Todo items has properties like name, date etc. so far so good. Now I want to have methods on objects which are non serializable. I.e. Todo.rename() which would rename todo and do a lot of other stuff.

    As far as I understand I can have function declared somewhere and do rename(Todo) or perhaps pass that function via props this.props.rename(Todo) to the component.

    I have 2 problems with declaring .rename() somewhere: 1) Where? In reducer? It would be hard to find all would be instance methods somewhere in the reducers around the app. 2) Passing this function around. Really? should I manually pass it from all the higher level components via And each time I have more methods add a ton of boilerplate to just pass it down? Or always do and hope that I only ever have one rename method for one type of object. Not Todo.rename() Task.rename() and Event.rename()

    That seems silly to me. Object should know what can be done to it and in which way. Is not it?

    What I'm missing here?

  • markerikson
    markerikson about 8 years
    You might want to take a look at github.com/tommikaikkonen/redux-orm, which puts a nice Model-class-like abstraction on top of immutable Redux state.
  • Dan Abramov
    Dan Abramov about 8 years
    We call this “selectors” and it’s indeed the recommended way to query the state, except that we don’t recommend to put them onto the state. Please see shopping-cart example in the Redux repo. For efficient memoization, we recommend using github.com/reactjs/reselect as described in Computing Derived Data recipe.
  • Charles Haro
    Charles Haro about 8 years
    But what if you want to do something like "get all visible todo" items, where would you put a filter function? Is it appropriate to add that function to the state? Or is that frowned upon?
  • Dan Abramov
    Dan Abramov about 8 years
    That is frowned upon. Instead we recommend writing this function separately to accept the state argument. We call such functions “selectors”, and in fact they are important for optimizations in Redux. Check out redux.js.org/docs/recipes/ComputingDerivedData.html
  • DDS
    DDS over 7 years
    Redux does not convert your state to pain objects at all. Your state ends up having a class instance stored. It won't be s pain object.
  • Josh
    Josh over 7 years
    In most object-oriented code, methods are used to get data as well as to set it. For example, a Todo item might have .getName(). Some of these may have logic: a getUrgency() method might look at the tasks due date and its priority, and do some math to return an answer. Where should these functions be defined, if not as methods on the objects in the store?
  • Venryx
    Venryx about 7 years
    @DDS From what I've seen, this is false. (I'm too lazy to prove it atm, though : P)
  • DDS
    DDS about 7 years
    @Venryx Apart from my typos, what I said was entirely correct and I'm not too lazy to prove it. Here's the code that sets the reducer straight from what you pass to createStore and here's the code that calls it and takes whatever you return and stores it as the state. Without looking at it or converting it to plain objects.
  • adailey
    adailey about 7 years
    Yes @Venryx is right, redux does not convert objects. What caused the redux store to end up with plain objects in my case (and probably why DDS has seen it as well) is that I am doing server side rendering and then serializing the redux store to pass it from the server side to the client side. The serialization process converts everything in the store to plain objects.
  • TrySpace
    TrySpace about 7 years
    @DanAbramov I'm curious to the answer to josh 's last question. I'm dealing with a similar situation where I have a sum of some properties, but where to put that instead of a Class model .method? In an Effect module? In the selector? What would be the best? Check my more specific question: stackoverflow.com/questions/43489294/… I'm trying to prevent putting a new Model() onto the store...
  • John O'Sullivan
    John O'Sullivan almost 7 years
    Another use-case where preserving methods would be nice: I'm loading data into my Redux store from a Meteor subscription. Meteor Collection objects let you define lots of helper methods which transform data, such as returning a user's full name by concatenating the first and last names. It'd be really nice if we could preserve those.
  • Vectorjohn
    Vectorjohn over 6 years
    If you have the need for helpful methods on objects stored in your state, define a module named "User" for example, and put in there functions that do the helpful things like User.fullName(user). or "User(user).fullName()". Do it however you want, but the data in the store should be plain objects.
  • Rajkumar Bansal
    Rajkumar Bansal almost 5 years
    @Vectorjohn idea seems right as I am also using the same in my project and works like a charm but remember you don't need to make that module a react component, instead, make it a simple class and connect that class with reducers using store.getState() and start making those helper functions.