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
Another approach I found to work (although not ideal) is to use
structuredClone
to create a deep copy oflayout
that is managed by redux, and then supply that deep copy to thePlot
component.It seems that the
Plot
component fromreact-plotly.js
doesn’t work withredux-toolkit
maintained state slices which disallow, or guard, against state mutations. I had noticed that thePlotReactState
had some additional "state" properties that were never declared nor updated in your code.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
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.
Unfortunately
redux-toolkit
still does a mutation check so when you try to update the title there will be an invariant violation.You can try configure the store to skip the immutability check. See Immutability Middleware for details.
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.