Reputation: 106
Situation: I'm attempting to write a "tabbed" application, where each tab's view is based on a complex and stateful component. I'm able to organize and render this, however the state in each tab's component does not persist - e.g. if I have two tabs derived from the same component (each one containing a stateful <input />
child), and I edit the <input />
in one, it will be wiped clean when I navigate to the other tab and back.
I've been through a number of separate implementation attempts. At first, I thought I'd store the tab components in a Redux store - but Redux (understandably) doesn't like this, as they're not serializable. I then tried the solution recommended in https://github.com/reduxjs/redux/issues/1793 - storing unique keys in the Redux store, and using those to reference a collection of components which are maintained in a parent component's state (parent to the tabs). I.e. the tabs kept in my store have a .uid
property, which I use to retrieve the associated page component in my Content
parent component via contentPages[uid]
(where contentPages
is just an object of {[uid]: Component}
pairs). At this point, I don't understand React well enough to know why the component's states aren't persisting. I've tried adding unique key
s in various places thinking that might have been the issue, but it doesn't seem to change anything. (If it matters, I'm using functional components for everything).
Question: How do I persist complex React components and their associated state?
Example Code:
function Content() {
const [contentPages, setContentPages] = useState({});
const uid = useSelector(activeTabUidSelector));
...
return (
<div className="parent">
<Tabs /> // Renders the actual tabs
<div className="content-display">{contentPages[uid]}</div> // Renders the content of the current active tab
</div>
)
}
Where contentPages[uid]
could be any number of components, but in the case I'm working through happens to look something like the following:
function NewTab() {
const [text, setText] = useState("");
const handleChange = (event) => setText(event.target.value);
return (
<div className="new-tab">
<input value={text} onChange={handleChange} />
</div>
)
}
Upvotes: 2
Views: 221
Reputation: 106
Answer: After quite a lot of researching, scrounging, and many failed experiments, I've come to the conclusion that what I was trying to do was impossible. Which is a little bit of a shame, as this has forced me to implement a more complex state-management system.
To clarify, for anyone reading in the future, I was trying to keep a collection of stateful React components - only one of which was actively rendering in the DOM tree at any given time. I wished to preserve the state of these components, whether or not they were actively being rendered - but it would seem that React unloads components that are not being rendered, and their state along with them.
For anyone curious as to how I've implemented this functionality, I will continue to describe that (though I will not claim to know that this is the right/best way to do it).
Solution: I still manage state at the child component level - I just persist it within a Redux store, orchestrated at the parent component level. I'll illustrate this in an updated code example, mirroring the one in the question:
function Content() {
const [pages, setPages] = useState({});
const activeTab = useSelector(activeTabSelector));
const uid = activeTab.uid;
...
const Component = pages[uid];
// Where `saveTabState()` is an action in a Redux slice.
const saveState = (state) => dispatch(saveTabState({uid, state}));
const content = <Component
initState={activeTab.componentState}
saveState={saveState}
key={uid}
/>;
return (
<div className="parent">
<Tabs /> // Renders the actual tabs
<div className="content">{content}</div> // Renders the content of the current active tab
</div>
)
}
And the child component:
export function NewTab({initState, saveState}) {
const handleChange = (event) => {
saveState({...state, text: event.target.value});
};
return (
<div className="new-tab">
<input value={state.text} onChange={handleChange} />
</div>
)
}
export function getInitialState() {
return ({text: ""});
}
Note: (Alternatively, I could have simply hid the non-active tabs as suggested by @azundo. Perhaps this is the better solution - other posts suggest that this strategy is more performant, so maybe it's the way to go. It seems a bit counter-intuitive to me, to keep so much unrendered content in the DOM, but I'm also not the most experienced JavaScript dev. And it certainly would be easier to implement).
Though, one up-side to my strategy is that it persists all of the application state as serializable data in the Redux store - which is ideal (I think).
Upvotes: 3