Can I share a component instance across multiple trees in React? - javascript

I've been creating an API to help manage state machines in React.
It consists of three components:
<StateMachine>: Receives an xstate machine as a prop, and sets up a context for deeper components to use.
<StateView>: Receives two props: state & children, and renders its children only if that state is currently active.
<StateControl>: Receives some arbitrary props - each being an event used to transition the machine - and converts them to transition callbacks to be passed down to its children (which is NOT an element, but an elementType as determined by PropTypes).
Here is a visual representation of what is at play:
Using React's context API, I can flexibly switch on/off nodes in the React tree based on the state of the machine. Here's a sample code snippet demonstrating this:
const MyMachine = () =>
{
return (
<StateMachine machine={sampleMachine}>
<StateView state="initializing">
<StateControl onSuccess="success">
{MySampleInitializer}
</StateControl>
</StateView>
<StateView state="initialized">
<p>{"App initialized"}</p>
</StateView>
</StateMachine>
);
This works great! When the machine is in the "initializing" state, MySampleInitializer gets rendered. When initialization is complete, onSuccess is called which transitions the machine to "initialized". At this point, the <p> gets rendered.
Now the problem:
In most situations, each "state view" would render a different component (which gets created & mounted when the appropriate state becomes active).
However what if we wanted to apply the machine to a single component only? For example, I have a <Form> component which handles the rendering of some form elements, and should receive different props depending on the state the form is currently in.
const MyFormMachine = () =>
{
return (
<StateMachine machine={formMachine}>
<StateView state="unfilled">
<StateControl onFill="filled">
{(props) => <MyForm {...props} disableSubmit/>}
</StateControl>
</StateView>
<StateView state="filled">
<StateControl onClear="unfilled" onSubmit="submit">
{(props) => <MyForm {...props}/>}
</StateControl>
</StateView>
<StateView state="submitting">
<MyForm disableInput disableSubmit showSpinner/>
</StateView>
</StateMachine>
);
Using my current API, rendering a <MyForm> within each <StateView> will cause <MyForm> to be re-mounted anytime a state change happens (thereby destroying any internal state associated with it). The DOM nodes themselves will also be re-mounted, which may re-trigger things like autofocus (for instance).
I was hoping there may be a way to share the same <MyForm> instance across the various "views" such that this re-mounting does not occur. Is this possible? If not, is there an alternative solution which would fit with this API?
Any help greatly appreciated.
PS: If the question title is unsuitable, please suggest a change so that this question may more accessible. Thanks

The problem is that the components inside your StateView instances are always constructed regardless of whether you are in the correct state or not.
So in your form example, you always have 3 Form instances, but only 1 rendered at a time. As you've stated, you can only have 1 Form instance in order to maintain state and prevent re-mounting.
When you are passing a conditional component (MyForm) into another component (StateView), you should always wrap it inside a function.
Your StateView class can then only create the MyForm instance if you're in the correct state.
You now have only 1 instance at a time (assuming only 1 state is matched at a time), but each StateView still has its own instance which isn't shared.
From what I know, you cannot avoid separate instances unless inside the same parent component.
I would change your StateView component to handle multiple state checks instead of one. This way, your instances will be reused when the state changes (again, assuming only 1 state is matched at a time).
Your StateView construction could look something like this:
<StateMachine machine={formMachine}>
<StateView>
{{
"unfilled": () => (
<StateControl onFill="filled">
{(props) => <MyForm {...props} disableSubmit/>}
</StateControl>
),
"filled": () => (
<StateControl onClear="unfilled" onSubmit="submit">
{(props) => <MyForm {...props}/>}
</StateControl>
),
"submitting": () => (
<StateControl>
{(props) => <MyForm disableInput disableSubmit showSpinner/>}
</StateControl>
)
}}
</StateView>
</StateMachine>
Note that in order to reuse a component, the component has to be of the same type. I wrapped the 3rd MyForm in an empty StateControl so that each state constructs a StateControl, and therefore the components can be reused.
Also note that if you do have multiple states matched at a time inside a single StateView, you can give each StateControl a key property. The same key property value should not be used on two components that may be instantiated at the same time. An easier solution would be to just have separate StateView instances, where each one will only match a single state at a time.

Related

How to deal with duplicated function code that alters parent state

Say we got a Page-component that delegates the rendering of notifications to a Notification-component. The Page-component's render method contains the following ...
{this.state.notifications &&
<Notifications
notifications={this.state.notifications}
removeNotifAt={index => this.setState(prevState => {
const copy = [...prevState.notifications]
copy.splice(index, 1)
return { notifications: copy }
})}
removeNotifyBy={id => this.setState(prevState => {
const copy = [...prevState.notifications]
const index = copy.findIndex((notif, _) => { return notif.id === id })
copy.splice(index, 1)
return { notifications: copy }
})}
/>
}
... as you might notice, Notifications require some rather large function to alter the state of its parent. Since they access this.state, these functions have to be defined in the parent of Notifications, in this case Page.
Now, one can imagine that multiple pages have notifications that needs rendering and so they all have to code-duplicate the code snippet above. As we all know, code-duplication is bad, so how can we best avoid it?
It's impossible to extract the functions removeNotifyAt and removeNotifyBy out into functions defined in, say, Notifications.js since they need to access this.state.
So, what's the react-way of dealing with such duplicate functions that you can't extract away because it needs to access this.state? I suppose I am not the first one stumpling upon this, giving how trivial of a case this is.
You can move the code from removeNotifyBy and removeNotifyBy into functions, and place them in the parent component. Then you can pass them into the child components as props.
You could extract those functions into Notifications.js. (That is in case you don't wish to write those functions inside parent component).
Both the parent's state as will as the function that sets the state can be passed as props to Notifications component.
Example:
<Notifications parentState={this.state} parentStateHandler={this.setState} />
//Note: Ideally props shouldn't be named like this and one must avoid passing entire state object as prop, rather you should split it into props that the child component requires. However this is just to give you the clarity regarding how to access parent's state in child
Now Notifications component has entire parents state which can be accessed by props.parentState and you can also set Parent state by using props.parentStateHandler instead of this.setState

What counts as mutating state in React?

Background is at the top, my actual question is simple enough, at the bottom, but I provided the context in case I'm going about this totally wrong and my question turns out to not even be relevant. I have only been using react for about two weeks.
What I'm trying to do is create a singleton, re-usable backdrop that can be closed either by clicking it, or by clicking a control on the elements that use a backdrop. This is to avoid rendering multiple backdrops in multiple places in the DOM (e.g. grouping a backdrop with each different type of modal, side drawer or content preview) or have multiple sources of truth for the state of the backdrop.
What I've done is create the Backdrop itself, which is not exported
const Backdrop = props => (
props.show ? <div onClick={props.onClose} className={classes.Backdrop}></div> : null
);
I've also created a backdrop context, managed by a WithBackdrop higher order class component which manages the state of the backdrop and updates the context accordingly
class WithBackdrop extends Component {
state = {
show: true,
closeListeners: []
}
show() {
this.setState({ show: true });
}
hide() {
this.state.closeListeners.map(f => f());
this.setState({ show: false, closeListeners: [] });
}
registerCloseListener(cb) {
// this.setState({ closeListeners: [...this.state.closeListeners, cb]});
// Does this count as mutating state?
this.state.closeListeners.push(cb);
}
render() {
const contextData = {
isShown: this.state.show,
show: this.show.bind(this),
hide: this.hide.bind(this),
registerCloseListener: this.registerCloseListener.bind(this)
};
return (
<BackdropContext.Provider value={contextData}>
<Backdrop show={this.state.show} onClose={this.hide.bind(this)} />
{this.props.children}
</BackdropContext.Provider>
);
}
}
export default WithBackdrop;
I've also exported a 'backdropable' HOC which wraps a component with the context consumer
export const backdropable = Component => (props) => (
<BackdropContext.Consumer>
{value => <Component {...props} backdropContext={value}/>}
</BackdropContext.Consumer>
);
The usage of this API would be as follows: Wrap the part of your Layout/App that you want to potentially have a backdrop, and provide the context to any component that would activate a backdrop. 'Backdropable' is a just a lazy word I used for 'can trigger a backdrop' (not shown here, but I'm using TypeScript and that makes a little more sense as an interface name). Backdropable components can call show() or hide() and not have to worry about other components which may have triggered the backdrop, or about multiple sources of truth about the backdrop's state.
The last problem I had, however, was how to trigger a backdropable components close handler? I decided the WithBackdrop HOC would maintain a list of listeners so that components that need to react when the backdrop is closed by clicking the backdrop (rather than by that backdropable component's close button or something). Here is the modal component I'm using to test this
const modal = (props) => {
props.backdropContext.registerCloseListener(props.onClose);
return (
<div
className={[
classes.Modal,
(props.show ? '' : classes.hidden)
].join(' ')}>
{props.children}
<button onClick={() => {
props.onClose();
props.backdropContext.hide()
}}>Cancel</button>
<button onClick={props.onContinue}>Continue</button>
</div>
)
}
export default backdropable(modal);
As far as I understand, it is best practice to never mutate state. My question is, does pushing to an array maintained in state count as mutating state, and what potentially bad consequences should I expect from this? Should I copy the array into a new array with the new element every single time, or will I only get undefined React behaviour if I try to change the reference of a state member. As far as I understand react only shallowly compares previous and next state to determine re-renders and provides utilities for more complicated comparisons, and so this should be fine right? The reason is that the array copying method triggers a re-render, then the modal tries to re-register the closeListener, then WithBackdrop tries to add it again...and I get an infinite state update loop.
Even if there is nothing wrong with simply pushing to the same array, do you think there is a better way to go about doing this?
Thanks, I sincerely appreciate the efforts anyone who tries to answer this long question.
EDIT: this.setState({ closeListeners: [...this.state.closeListeners, cb]}); results in an infinite state-update loop.
Mutating state in React is when you change any value or referenced object in state without using setState.
As far as I understand, it is best practice to never mutate state. My
question is, does pushing to an array maintained in state count as
mutating state,
Yes
and what potentially bad consequences should I expect from this?
You can expect to change the value of state and not see the ui update.
Should I copy the array into a new array with the new element every
single time,
Yes:
const things = [...this.state.things]
// change things
this.setState({ things })
or will I only get undefined React behaviour if I try to
change the reference of a state member. As far as I understand react
only shallowly compares previous and next state to determine
re-renders and provides utilities for more complicated comparisons,
and so this should be fine right?
It will compare if you call setState and update if necessary. If you do not use setState, it won't even check.
Any changes directly to the state (without setState()) = mutating the state. In your case it is this line:
this.state.closeListeners.push(cb);
As #twharmon mentioned, you change the values in the memory but this does not trigger the render() of your component, but your component will eventually updated from the parent components leading to ugly and hard to debug side effects.
The solution for your problem using destructuring assignment syntax:
this.setState({
closeListeners: [...this.state.closeListeners, cb]
});
PS: Destructuring also helps to keep your code cleaner:
const Backdrop = ({ show, onClose }) => (
show ? <div onClick={onClose} className={classes.Backdrop}></div> : null
);

React changing state in Parent , does render calls for all its children and sub-children as iteration?

i am setting the language name in my local storage , when it changes from a dropdown in topbar , i want the whole current view to be re-rendered and words translated to the selected language. my layout is like this
render(){
return (
<MainContainer>
<TopBar/>
<SideBar/>
<RouteInsideSwitch/>
</MainContainer>
)
}
in render of components ,the words to be translated basically calls a function that returns the correct word based on the local storage language name.
i change the language and i set the state in maincontainer for selected langauge and set it in local storage. however i dont want to move that state from Maincontainer to all my components. also dont want to store it in redux because then all the possible containers have to listen to it and then pass it to their children as props.
what currently happens is that saving state in mainContainer without passing it to any children , the children does re-render but only the immediate ones , if there are more children in those children and so on , it does not re-render because i m not passing the state throughout the chain.
open to any suggestion based on different pattern for language changing. but my question is that is there any way to re-render the current open view (all components in dom).
If your concern is that you have a number of "possible containers" which all need to handle the state change, perhaps consider creating a higher order component that includes the common language rendering logic (your RouteInsideSwitch leads me to believe this may the issue). In that way, you can avoid duplicating that logic across a ton of "possible" components that all require the functionality of dynamic language rendering and will avoid the need to dial a bunch of components into a redux store, assuming they are in the same hierarchy.
const DynamicLanguageComp = RenderComponent => {
return class extends Component {
constructor(props) {
super(props)
//additional state setup if needed
}
changeLangFunc = () => { /* handle change */ }
render() {
return <RenderComponent handleLanguageChange={this.changeLangFunc} {...this.props} {...this.state} />
}
}
}
If you would like to avoid a re-render on certain intermediate components that may be receiving props by way of state change you can implement the lifecycle method shouldComponentUpdate(), which by default returns true. You can make a comparison of nextProps to your current props, and return false if a re-render is undesired despite new props.

Share data between React components with no relation?

I'm working on a React component library that allows for client-side data filtering by passing an array of objects and an <input/> as props to a <SearchFilter/> component. I want to return the filtered results to a separate <SearchResults/> component that can be rendered elsewhere in the tree (i.e. the results component doesn't have to be a child of the input component).
I've got the filtering figured out, but I'm not sure the best route to take in React on getting the filtered data to the <SearchResults/> component.
This is what I'd like to end up with...
<SearchFilter
data={data}
input={<input type="text" value={value} onChange={this.handleChange}/>}
/>
Then, using Render Props to return the data and map over that to return JSX, there would be the results component. Something like this...
<SearchResults
render={data => (
data.map(el => (
<div>
<span>{data.someProperty}</span>
</div>
)
)}
/>
This is what I'd like to achieve because I want to allow for rendering the <SearchFilter/> component at one place in the tree, and allow the <SearchResults/> component to be rendered elsewhere, so that there's maximum flexibility in how the tree is composed and, therefore, how the view is rendered.
I've looked into the Context API, but it seems like that would require a handful more components to be a part of my library, which further complicates what I'm trying to achieve. If that's the only way around it, then that's fine, but I wanted to ask and see if anyone can think of another solution.
Thanks!
The bigger issue is that you will need to manage a state that is shared between components on a higher level, i.e., any component that will wrap these other two components, ultimately. With plain React, this state would be managed by the parent (or ancestor) component, passing down the relevant values as props. This opposed to the, usually bad, idea to have sibling components influence each other's state, since you well get into the "who's boss here"-problem.
The thing the Context API handles is not having to pass down props for things that typically don't change (or: typically shouldn't cause renders to trigger often).
A global state store, such as Redux, can help you modelling this, but in essence it's not much more than 'a' component managing state, and other components rendering according to that state. Events within the lower components trigger changes in the data, which will cause the state to change, which will cause the props of the children to change, which then will cause re-renders.
I'd advise you to try using this simple pattern:
class Search ... {
state = {data: [], text: ""}
render() {
return (
<div>
<SearchFilter
data={this.state.data}
onSearch={() => this.fetchNewData()}
onChange={(e) => this.setState({text: e.targetElement.value})}
text={this.state.text}
/>
<SearchResults data={this.state.data} />
</div>
);
}
fetchNewData() {
fetch('/url?text=' + this.state.text)
.then((newData) => { this.setState({data: newData}); })
}
}
Something along these lines. If you have trouble modelling stuff like this, you can use Redux to force you to do it in a similar way, and avoid managing local state intermixing with global state (which is typically something that is hard to manage).
If you do this right, components that have no state (i.e., aren't responsible for managing state and thus have no event handlers) can all become pure components, i.e. stateless components, i.e. functions that return JSX based on props:
const SearchResults = ({data}) => <div>{data.map( () => <etc /> )}</div>
You could create a data store class that holds your filter, pass it in as a property to both components, and have your SearchFilter component change a value in that.

Functional difference: state variable vs instance variable with setState({})

This question has been asked multiple times and discussed elsewhere on internet as well. However I could not find any reason as to why state would be better for some data than instance variable, except to maintain some separation.
One could substitute -
this.setState({variableA: "xyz"}); // access variable as this.state.variableA
with
this.variableA = "xyz"; // access variable as this.variableA
this.setState({});
Is there any 'functional difference' in the above two code options?
I have a situation where maintaining state variables would create more problems than providing separation would compensate for.
I have two components with shared 'states' i.e. changing something in component A changes rendering of component B and vice-versa - they both take inputs. Now if I were to maintain states, I will need callback functions both ways that can keep the shared state synchronised. It seems much easier to have the shared 'state' passed as an object reference in props and modify it on input and call setState({}) on both the components to re-render - we do not need to synchronise anymore. Is there any functional downside to this?
For e.g. consider -
class A extends React.component {
render() {
return (
<div onClick={() => {
this.props.data.reset();
this.setState({});
}}>
<B data={this.props.data} onDataChange={() => {
this.setState({});
}}/>
{/* ... */}
{this.props.data.value}
</div>
);
}
}
class B extends React.component {
render() {
return (
<input
type="text"
onChange={event => {
this.props.data.value = event.target.value;
this.props.onDataChange();
}}
value={this.props.data.value}
/>
);
}
}
I do not want to merge A and B as they both are significantly complex (the code above is just an example to show shared 'state').
Is there any functional issue in not capturing this.props.data in state variable?
You might wanna lift the state up, i.e. move this shared state into their common ancestor and use props. That's basically what you're doing but without exposing the state outside of your components, which is not recommended.

Categories