Search code examples
reactjsreduximmer.js

React and Redux with Immer.js


I'm having a question to Immer.js with React.js and Redux. I'm familiar with React.js. As far as I know it is not "allowed" to update a property itself by

props.someValue= 'someValue'

So you need to pass a callback function to the component itself like

<SomeComponent
   key={"componentKey"}
   someValue={"someValue"}
   onSomeValueChange={this.handleSomeValueChange.bind(this)}
/>

and in SomeComponent you call the function like this:

...
this.props.onSomeValueChange('someNewValue');
...

Or you can handle this with Redux:

import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { updateSomeValue } from '../actions/index';

function SomeComponent(props) {

    function onTextChanged(event) {
        props.updateSomeValue(event.target.value);
    }

    return (
        <>
            <input type="text" value={someValue} onChange={onTextChanged} />
        </>
    )
}

function mapStateToProps({ someValue }) {
    return {
        someValue
    };
}

function mapDispatchToProps(dispatch) {
    return {
        updateSomeValue: someValue => dispatch(updateSomeValue(someValue)),
    };
}
 
const Form = connect(
    mapStateToProps,
    mapDispatchToProps
)(SomeComponent);
 
export default Form;

The code above is a very simple example, because someValue is just a string and no complex object. Now, if it is getting more complex and you have objects with subobjects, I'm still searching for the best way to update the subobjects.

In the past, I've used lodash to create a "clone" of the object and modified the "clone", before I updated the original property.

function onTextChanged(event) {
    let updatedJob = _.cloneDeep(props.currentJob);
    for(let a = 0; a < updatedJob.job_texts.length; a++) {
        if(updatedJob.job_texts[a].id === props.jobText.id) {
            updatedJob.job_texts[a].text = event.target.value;
        }
    }
    props.updateCurrentJob(updatedJob);
}

This solution did work, but as you can see, it is probably not best way to handle this. Now I read today, that my solution is not recommended as well. You need to create a "copy" of each subobject as far as I understood. Then I stumbled across redux page about immer.js, but I'm not quite sure, how to use this:

The situation is as follows: I have an object currentJob, which have several properties. One of these properties is a subobject (array) called job_texts. One job_text has a property text which I need to update.

I thought, I can handle this, this way:

let updatedJob = props.currentJob;
props.updateCurrentJob(
    produce(updatedJob.job_texts, draftState => {
        if(draftState.id === props.jobText.id) {
            draftState.text = text;
        }
    })
);

...but of course this won't work, because with the code above I'm updating currentJob with the subobject array. How can I use immer.js to update one job_text within the object currentJob?


Solution

  • The best way would be to use the official Redux Toolkit, which already includes immer in reducers created with createReducer and createSlice.

    It's also generally the recommended way of using Redux since two years.

    Generally, you should also have that "subobject update logic" in your reducer, not your component.

    So with RTK, a "slice" would for example look like

    const objectSlice = createSlice({
      name: 'object',
      reducers: {
        updateSubObjectText(state, action) {
          const jobText = state.job_texts.find(text => text.id === action.payload.id)
          if (jobText) {
            jobText.text = action.payload.text
          }
        }
      }
    })
    
    export const { updateSubObjectText } = objectSlice.actions
    export default objectSlice.reducer
    

    This will create a reducer and an action creator updateSubObjectText to be used in your component. They will be hooked together and action types are an implementation detail you do not care about.

    In your component you would now simply do

    dispatch(updateSubObjectText({ id, text }))
    

    For more infos see this quick into to Redux Toolkit or the full official Redux "Essentials" tutorial which nowadays covers Redux Toolkit as the recommended way of writing real Redux logic.

    There is also a full docs page on writing Reducers with immer.