skip to Main Content

I’m using react-plotly.js and want to use redux to store the layout of my plot. I’m able to do it using react state, but the plot does not render when trying to do the same with redux.

I know that the redux state is updating correctly. I think my issue is related to the state management section in the link I provided above, but it’s not clear to me why it does not work with redux state, but it works with react state.

Below is what you need to reproduce my project.

# Create new vite project
npm create vite@latest test-app --template react

# Dependencies from package.json
"dependencies": {
  "@reduxjs/toolkit": "^1.9.3",
  "plotly.js": "^2.22.0",
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "react-plotly.js": "^2.6.0",
  "react-redux": "^8.0.5",
  "react-resize-detector": "^9.1.0",
}

Directory structure and code:

├── src
│   ├── App.jsx
│   ├── components
│   │   ├── PlotReactState.jsx
│   │   ├── PlotReduxState.jsx
│   │   └── plotSlice.js
│   ├── main.jsx
│   └── store
│       └── store.js

App.jsx

import PlotReactState from "./components/PlotReactState";
import PlotReduxState from "./components/PlotReduxState";

export default function App() {
  return (
    <div>
      <h4>Plot using react state</h4>
      <PlotReactState />
      <h4>Plot using redux state</h4>
      <PlotReduxState />
    </div>
  );
}

components/PlotReactState.jsx

import { useState, useEffect } from "react";
import Plot from "react-plotly.js";
import { useResizeDetector } from "react-resize-detector";

export default function PlotReactState() {
  const { width, ref } = useResizeDetector();
  const [title, setTitle] = useState("");

  const [layout, setLayout] = useState({
    width: width,
    title: {
      text: title,
    },
    xaxis: {
      title: "",
    },
    yaxis: {
      title: "",
    },
    paper_bgcolor: "lightgrey",
    plot_bgcolor: "lightgrey",
  });

  useEffect(() => {
    setLayout((prevLayout) => ({ ...prevLayout, width: width }));
  }, [width]);

  // Modify one way
  useEffect(() => {
    setLayout((prevLayout) => ({ ...prevLayout, title: { text: title } }));
  }, [title]);

  // Modify another way
  const handleChange = (value) => {
    setLayout((prevLayout) => ({ ...prevLayout, xaxis: { title: value } }));
  };

  return (
    <div ref={ref}>
      <input
        type="text"
        placeholder="Edit title"
        onChange={(event) => setTitle(event.target.value)}
      />
      <input
        type="text"
        placeholder="Edit x-axis title"
        onChange={(event) => handleChange(event.target.value)}
      />
      <Plot layout={layout} />
    </div>
  );
}

components/PlotReduxState.jsx

import Plot from "react-plotly.js";
import { useDispatch, useSelector } from "react-redux";
import { useResizeDetector } from "react-resize-detector";
import { updateTitle, updateXLabel } from "./plotSlice";

export default function PlotReduxState() {
  const dispatch = useDispatch();
  const { layout } = useSelector((state) => state.plot);
  const { width, ref } = useResizeDetector();

  return (
    <div ref={ref}>
      <input
        type="text"
        placeholder="Edit title"
        onChange={(event) => dispatch(updateTitle(event.target.value))}
      />
      <input
        type="text"
        placeholder="Edit x-axis title"
        onChange={(event) => dispatch(updateXLabel(event.target.value))}
      />
      <Plot layout={layout} />
    </div>
  );
}

components/plotSlice.js

import { createSlice } from "@reduxjs/toolkit";

const baseLayout = {
  title: {
    text: "",
  },
  xaxis: {
    title: "",
  },
  paper_bgcolor: "lightgrey",
  plot_bgcolor: "lightgrey",
}

const plotSlice = createSlice({
  name: "plot",
  initialState: {
    layout: baseLayout,
  },
  reducers: {
    updateTitle: (state, {payload}) => {
      state.layout = {
        ...state.layout,
        title: {
          text: payload
        }
      }
    },
    updateXLabel: (state, {payload}) => {
      state.layout = {
        ...state.layout,
        xaxis: {
          title: payload
        }
      }
    },
  },
});

export const { updateTitle, updateXLabel } = plotSlice.actions;

export default plotSlice.reducer;

store/store.js

import { configureStore } from "@reduxjs/toolkit";
import plotReducer from "../components/plotSlice";

export const store = configureStore({
  reducer: {
    plot: plotReducer
  },
});

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from "react-redux";
import App from './App'
import { store } from "./store/store";

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
)

2

Answers


  1. Chosen as BEST ANSWER

    Another approach I found to work (although not ideal) is to use structuredClone to create a deep copy of layout that is managed by redux, and then supply that deep copy to the Plot component.

    export default function PlotReduxState() {
      const dispatch = useDispatch();
      const { layout } = useSelector((state) => state.plot);
      const { width, ref } = useResizeDetector();
    
      // Create a deep copy of layout and supply
      // to the Plot component
      const layout2 = structuredClone(layout);
    
      return (
        <div ref={ref}>
          <input
            type="text"
            placeholder="Edit title"
            onChange={(event) => dispatch(updateTitle(event.target.value))}
          />
          <input
            type="text"
            placeholder="Edit x-axis title"
            onChange={(event) => dispatch(updateXLabel(event.target.value))}
          />
          <Plot layout={{ width, ...layout2 }} />
        </div>
      );
    }
    

  2. It seems that the Plot component from react-plotly.js doesn’t work with redux-toolkit maintained state slices which disallow, or guard, against state mutations. I had noticed that the PlotReactState had some additional "state" properties that were never declared nor updated in your code.

    export default function PlotReactState() {
      ...
    
      const [layout, setLayout] = useState({
        width: width,
        title: {
          text: title,
        },
        xaxis: {
          title: "",
        },
        yaxis: {
          title: "",
        },
        paper_bgcolor: "lightgrey",
        plot_bgcolor: "lightgrey",
      });
    
      ...
    
      return (
        ...
      );
    }
    
    {
      paper_bgcolor: "lightgrey",
      plot_bgcolor: "lightgrey",
      title: { text: '' },
      width: 865,
      xaxis: {
        autorange: true, // <-- mutated state
        range: [-1, 6],  // <-- mutated state
        title: {
          text: ""
        },
      },
      yaxis: {
        autorange: true, // <-- mutated state
        range: [-1, 4],  // <-- mutated state
        title: { text: '' },
      },
    }
    

    To test this theory I created a "legacy" reducer function, e.g. a vanilla Javascript function that simply computes the state and doesn’t protect it.

    store.js

    import { configureStore } from "@reduxjs/toolkit";
    // import plotReducer from "../components/plotSlice";
    
    const baseLayout = {
      title: {
        text: ""
      },
      xaxis: {
        title: ""
      },
      yaxis: {
        title: ""
      },
      paper_bgcolor: "lightgrey",
      plot_bgcolor: "lightgrey"
    };
    
    const plotReducer = (state = { layout: baseLayout }, action) => {
      switch (action.type) {
        case "plot/updateTitle":
          return {
            ...state,
            layout: {
              ...state.layout,
              title: {
                text: action.payload
              }
            }
          };
    
        case "plot/updateXLabel":
          return {
            ...state,
            layout: {
              ...state.layout,
              xaxis: {
                title: action.payload
              }
            }
          };
    
        default:
          return state;
      }
    };
    
    export const store = configureStore({
      reducer: {
        plot: plotReducer
      }
    });
    

    Running your same code but with this legacy reducer function, the redux state is now also mutated and the additional values injected into state, but the plot renders.

    enter image description here

    Unfortunately redux-toolkit still does a mutation check so when you try to update the title there will be an invariant violation.

    enter image description here

    You can try configure the store to skip the immutability check. See Immutability Middleware for details.

    export const store = configureStore({
      reducer: {
        plot: plotReducer
      },
      middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
          immutableCheck: {
            ignoredPaths: ["plot"]
          }
        })
    });
    

    I tried this. It works with the legacy reducer function to not warn/error on mutation, however, it didn’t work with the slice reducer.

    Either way, state mutations are bad and incorrect behavior in React. I’m surprised by this in such an established library.

    There is a Github issue regarding this library’s props mutation with some decent discussion.

    It’s up to you if you want to do this in Redux, and there’s the workaround I’ve described above. Using local component state seems to be a little easier though.

    Here’s the running codesandbox demo I created to test your code.

    Edit how-to-use-redux-with-react-plotly-js

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search