Retrieving latest state from functional child component using callbacks - reactjs

Coming from Vue and diving into React I seem to struggle with the concept of the hooks / component lifecycle and data flow. Where in Vue I could solve my issues using v-model in React I struggle to do so. Basically:
What I intend to do is : have a parent component which is a form. This component will have several child components where each are fragments of the form data. Each child component manages its own state. The parent component has a submit button that should be able to retrieve the values from all child components.
In a nutshell, my approach is: have a functional component to manage part of said form using state hooks. This "form fragment"-component can broadcast a change event broadcastChange() containing the updated values of its inputs. In the parent I have a submit button which invokes this broadcastChange() event from the child using a ref.
The problem I am running into is that I am always getting the default values of the child state. In the below example, I'd always be getting foo for property inputValue. If I were to add a submit button inside the child component to invoke broadcastChange() directly, I do get the latest values.
What am I overlooking here ? Also, if this is not the React way to manage this two-way communication, I'd gladly hear about alternatives.
Parent code:
function App() {
const getChildChangesFn = useRef( null );
const submitForm = e => {
e.nativeEvent.preventDefault();
getChildChangesFn.current(); // request changes from child component
};
const handleChange = data => {
console.log( data ); // will always list { inputValue: "foo" }
};
return (
<form>
<child getChangeFn={ getChildChangesFn } onChange={ handleChange } />
<button type="submit" onClick={ () => submitForm() }>Save</button>
</form>
);
}
Child code:
export default function Child( props ) {
const [ inputValue, setInputValue ] = useState( "foo" );
useEffect(() => {
// invoke inner broadcastChange function when getChangeFn is triggered by parent
props.getChangeFn.current = broadcastChange;
}, []);
const broadcastChange = () => {
props.onChange({ inputValue });
};
render (
<fieldset>
<input
type="text"
value={ inputValue }
onChange={ e => setInputValue( e.target.value ) }
/>
</fieldset>
);
}

You need to leave Vue behind and make your thinking more React-y. Rather than trying to manage your state changes imperitavely by 'broadcasting' them up, you need to 'lift your state' (as they say) in to your parent and pass it down to your children along with change handlers.
A simple example:
export default function App() {
const [childState, setChildState] = useState(false);
const onChildClick = () => setChildState((s) => !s);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<ChildComponent childState={childState} onClickHandler={onChildClick} />
</div>
);
}
const ChildComponent = ({ childState, onClickHandler }) => {
return (
<button onClick={onClickHandler}>
State is {childState ? "true" : "false"}
</button>
);
};
Sandbox here

You have to uplift the state to parent component, or use a state manager like redux. In your case you are already passing down an onChange function, which should do what you need. But on parent component you need to manage the state, and store changes in the state, and pass it down to components as props.
An alternative to redux is mobx, which is a reactive library, in your case, sounds like you are familiar with reactive components, might fit you better. Other alternatives are using the Context that comes with react, and react-query is also a solid alternative, as it also handles async api calls.

Related

In react world, when does props changing happen? It's definitely triggered by parent component re-rendering?

When it comes to thinking about possibility of props changing, should I only care about parent component updating? Any other possible cases exist for props changing?
I can guess only one case like the code below. I would like to know if other cases exist.
const ParentComponent = () => {
const [message, setMessage] = useState('Hello World');
return (
<>
<button onClick={() => setMessage('Hello Foo')}>Click</button>
<ChildComponent message={message} />
</>
);
};
const ChildComponent = ({ message }: { message: string }) => {
return <div>{message}</div>;
};
Props can change when a component's parent renders the component again with different properties. I think this is mostly an optimization so that no new component needs to be instantiated.
Much has changed with hooks, e.g. componentWillReceiveProps turned into useEffect+useRef (as shown in this other SO answer), but Props are still Read-Only, so only the caller method should update it.

How to pass a method into a child element if the method relies on state variables in React

I've been learning react over the last few days and for the most part it makes sense, however there is one thing that is stumping me.
If I have
A Parent element with some state variables and a callback method
A child element that takes a callback method as a prop
The callback method relies on some piece of state that is in the parent element
I don't want to re-create the view object every time any state changes
Every time I try to do this, it seems like the child element is calling some older version of the parent element (presumably the instance of the parent that actually created the child) instead of the current version.
I'm getting the feeling that what I want is just wrong on a fundamental level and isnt The React Way
The reason that I am trying to do this is that my main parent contains 17 divs, each of which represent a key on a musical instrument, and each of which contains at least 20-30 divs. The lowest div (of which there are at least a few hundred) has an onClick event that I want to modify the functionality of based on whether modifier keys are held down (shift, control etc).
Currently I have Raised the state of the shiftPressed to be on the single parent element then passed down the value of that into each child through props, however re-rendering hundreds of divs whenever a user pushes shift takes quite a while.
I've made a code sandbox to show the current problem sandbox
Sandbox code:
import "./styles.css";
import { useState, useEffect, useRef } from "react";
export default function App() {
//Our state holding data
const [state, setState] = useState(false);
//Our state holding the view
const [view, setView] = useState(<div></div>);
const printState = useRef(null);
//Component did mount hook
useEffect(() => {
reGenerate();
}, []);
//state update hook
useEffect(() => {
printState.current();
}, [state]);
//function to flip the state
const flipState = () => {
setState(!state);
};
//The method that updates the view
//(The idea being that I don't want to update the view on every state change)
const reGenerate = () => {
setView(
<>
<p>
State: {state && "true"} {state || "false"}
</p>
<Child callback={printState} />
</>
);
};
//Method for validation
printState.current = () => {
console.log("Printed state: " + state);
};
return (
<div className="App">
<h1>Parent-child-prop-problem (prop-lem)</h1>
<ol>
<li>click "force regeneration"</li>
<li>
click "flip state" and the value of state after the flip will be
printed in console, but it won't show up on the HTML element
</li>
<li>
Click "print state (from child)" and observe that the console is
printing the old version of the state
</li>
</ol>
<button onClick={flipState}>Flip State</button>
<button onClick={reGenerate}>Force Regeneration</button>
{view}
</div>
);
}
function Child(props) {
return (
<div>
<button onClick={props.callback.current}>Print State (from child)</button>
</div>
);
}
Taking a quick peek at your sandbox code and I see that you are storing JSX in state, which is anti-pattern and often leads to stale enclosures like you describe.
I don't want to re-create the view object every time any state changes
"Recreating" the view is a necessary step in rendering UI in React as a result of state or props updating. State should only ever store data and the UI should be rendered from the state. In other words, treat your UI like a function of state and props. Toggle the state state value and render the UI from state.
Example:
export default function App() {
//Our state holding data
const [state, setState] = useState(false);
const printState = useRef(null);
//state update hook
useEffect(() => {
printState.current();
}, [state]);
//function to flip the state
const flipState = () => {
setState(!state);
};
//Method for validation
printState.current = () => {
console.log("Printed state: " + state);
};
return (
<div className="App">
<h1>Parent-child-prop-problem (prop-lem)</h1>
<ol>
<li>
click "flip state" and the value of state after the flip will be
printed in console, but it won't show up on the HTML element
</li>
<li>
Click "print state (from child)" and observe that the console is
printing the old version of the state
</li>
</ol>
<button onClick={flipState}>Flip State</button>
<p>State: {state ? "true" : "false"}</p>
<Child callback={printState} />
</div>
);
}
function Child(props) {
return (
<div>
<button onClick={props.callback.current}>Print State (from child)</button>
</div>
);
}
It's also generally considered anti-pattern to use any sort of "forceUpdate" function and is a code smell. In almost all circumstances if you hit a point where you need to force React to rerender you are doing something incorrect. This is the time you step back and trace through your code to find where a certain piece of state or some prop isn't updated correctly to trigger a rerender naturally.

How to pass data from child component to parent component without the child component rerendering in React?

My child component is an accordion with controls in it. The problem is, everytime you input data on the controls and it triggers an onChange event which passes its data to its parent component, the accordion closes/collapses and I don't need that behavior.
The accordion is just imported from a library and I can't change its behavior.
Is there anyway to prevent this or pass props/data from child component to parent component without rerendering/restarting DOM of child component?
Parent component:
const ParentComponent = () => {
const handleChangeChildAccordion = (e) => {
console.log(e);
}
return (
<ChildComponentAccordion currentData={currentData} onChangeTPAccordion={handleChangeChildAccordion}/> }
);
}
Child Component:
const ChildComponent = (props) => {
const onDataChangeHandler = (e) => {
props.onChangeTPAccordion(e);
};
return (
<Accordion onChange={onDataChangeHandler}/>
);
}
The child component doesn't remount, it rerenders. Normally, a component rerenders any time any its parent rerenders. To avoid that you:
Avoid changing the child component's props, and
Memoize the child component so it doesn't rerender when its props don't change in a way it cares about.
You do the first by ensuring that, for instance, callbacks you pass the component are stable (they don't change on every render). In a function component, you do it by using useMemo, useCallback, and/or useRef. In a class component, you typically do that by making the callbacks properties on the component instance that aren't recreated.
You do the second by using React.memo (if it's a function component) or for a class component by extending PureComponent (for simple memoization) or implementing shouldComponentUpdate.
You haven't given us much code to work with, so here's a simple example:
const { useState, useCallback, useRef } = React;
const log = (...msgs) => console.log(...msgs);
const Child1 = React.memo(({value, doUpdate}) => {
log(`Child1 rendering`);
return <div>
Child1 - Value: {value} <input type="button" value="+" onClick={doUpdate} />
</div>;
});
const Child2 = React.memo(({value, doUpdate}) => {
log(`Child2 rendering`);
return <div>
Child2 - Value: {value} <input type="button" value="+" onClick={doUpdate} />
</div>;
});
const Parent = () => {
log(`Parent rendering`);
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
// `useCallback` remembers the first function you pass it and returns
// it to you, only returning the new one you pass it if the
// dependencies array changes. This is one way to create a stable
// callback.
const update1 = useCallback(() => setCounter1(c => c + 1), []);
// `useRef` gives you an object you can put arbitrary properties on,
// which is another way to have a stable callback.
const update2Ref = useRef(null);
if (!update2Ref.current) {
update2Ref.current = () => setCounter2(c => c + 1);
}
const update2 = update2Ref.current;
return <div>
<Child1 value={counter1} doUpdate={update1} />
<Child2 value={counter2} doUpdate={update2} />
</div>;
};
ReactDOM.render(<Parent />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
TypeScript playground with types added
That shows two different ways of making acallback stable (useCallback and useRef). (useMemo is what useCallback uses under the hood, so I didn't do an example of it.)
In that example, notice how the child telling the parent to update its counter doesn't cause the other child to re-render. That's because:
We don't change the other child's props (because our callbacks are stable and the other child's counter didn't change), and
We've memoized the child components
You can customize whether the component re-renders by passing a second argument to React.memo which is a function that says whether the new props coming in are "the same" (for rendering purposes) as the previous ones. By default, React.memo does a simple === comparison on each prop. And again, for class components, PureComponent does the simple === comparison, or you can implement shouldComponentUpdate yourself for more finely-tuned checks.

Update parent state variable from child React Reactive Form

I am trying to explore react library with next framework. Since I am an angular developer, I like to reuse some of my reactive-form to my new application. I found this library since it has almost the same implementation of reactive-form.
Now, I am using state variable on my parent form; however, whenever I try to update the value from child (which is the reactive form). I cannot accomplish it.
Here's my simple code to replicate this.
import React, { useState } from "react";
import { FieldGroup, FieldControl } from "react-reactive-form";
export default function App() {
const [isRegistered, setIsRegistered] = useState(false);
async function submitForm(e) {
e.preventDefault();
setIsRegistered(state => !state);
console.log(isRegistered);
//await function call .....
}
return (
<div>
<h1>Hello StackBlitz!</h1>
<FieldGroup
control={form}
render={({ get, invalid }) => (
<form onSubmit={submitForm}>
<FieldControl
name="email"
render={TextInput}
meta={{ label: "Email" }}
/>
<button type="submit">Submit</button>
<p>{isRegistered.toString()}</p>
{isRegistered ? <span>Registered Successfully</span> : null}
</form>
)}
/>
</div>
)
}
Just to keep it short, the form and TextInput is just a model and an element.
As you can see., I am updating my state variable on the submitForm function by putting it as an onSubmit function of my form; however, I am able to trigger the submitForm whenever I am trying to submit, but the state variable value doesn't change.
The thing is, when I try to call the submitForm function outside the child (FieldGroup), I am able to update the value.
I created a sample app so you can check as well.
It seems like you need to set strict prop to false for FieldGroup, like described here: https://github.com/bietkul/react-reactive-form/blob/master/docs/api/FieldGroup.md
strict: boolean;
Default value: true
If true then it'll only re-render the component only when any change happens in the form group control irrespective of the parent component(state and props) changes.
I don't know this library, but to me it just looks like the FormGroup is not re-render, because none of it's props are being changed.
The documentation says that passing strict={false} to the <FieldGroup /> component should allow it to re-render when the parent component updates as well. In your given example (thanks for making an example) that also does the trick.

How can I reset a react component including all transitively reachable state?

I occasionally have react components that are conceptually stateful which I want to reset. The ideal behavior would be equivalent to removing the old component and readding a new, pristine component.
React provides a method setState which allows setting the components own explicit state, but that excludes implicit state such as browser focus and form state, and it also excludes the state of its children. Catching all that indirect state can be a tricky task, and I'd prefer to solve it rigorously and completely rather that playing whack-a-mole with every new bit of surprising state.
Is there an API or pattern to do this?
Edit: I made a trivial example demonstrating the this.replaceState(this.getInitialState()) approach and contrasting it with the this.setState(this.getInitialState()) approach: jsfiddle - replaceState is more robust.
To ensure that the implicit browser state you mention and state of children is reset, you can add a key attribute to the root-level component returned by render; when it changes, that component will be thrown away and created from scratch.
render: function() {
// ...
return <div key={uniqueId}>
{children}
</div>;
}
There's no shortcut to reset the individual component's local state.
Adding a key attribute to the element that you need to reinitialize, will reload it every time the props or state associate to the element change.
key={new Date().getTime()}
Here is an example:
render() {
const items = (this.props.resources) || [];
const totalNumberOfItems = (this.props.resources.noOfItems) || 0;
return (
<div className="items-container">
<PaginationContainer
key={new Date().getTime()}
totalNumberOfItems={totalNumberOfItems}
items={items}
onPageChange={this.onPageChange}
/>
</div>
);
}
You should actually avoid replaceState and use setState instead.
The docs say that replaceState "may be removed entirely in a future version of React." I think it will most definitely be removed because replaceState doesn't really jive with the philosophy of React. It facilitates making a React component begin to feel kinda swiss knife-y.
This grates against the natural growth of a React component of becoming smaller, and more purpose-made.
In React, if you have to err on generalization or specialization: aim for specialization. As a corollary, the state tree for your component should have a certain parsimony (it's fine to tastefully break this rule if you're scaffolding out a brand-spanking new product though).
Anyway this is how you do it. Similar to Ben's (accepted) answer above, but like this:
this.setState(this.getInitialState());
Also (like Ben also said) in order to reset the "browser state" you need to remove that DOM node. Harness the power of the vdom and use a new key prop for that component. The new render will replace that component wholesale.
Reference: https://facebook.github.io/react/docs/component-api.html#replacestate
The approach where you add a key property to the element and control its value from the parent works correctly. Here is an example of how you use a component to reset itself.
The key is controlled in the parent element, but the function that updates the key is passed as a prop to the main element. That way, the button that resets a form can reside in the form component itself.
const InnerForm = (props) => {
const { resetForm } = props;
const [value, setValue] = useState('initialValue');
return (
<>
Value: {value}
<button onClick={() => { setValue('newValue'); }}>
Change Value
</button>
<button onClick={resetForm}>
Reset Form
</button>
</>
);
};
export const App = (props) => {
const [resetHeuristicKey, setResetHeuristicKey] = useState(false);
const resetForm = () => setResetHeuristicKey(!resetHeuristicKey);
return (
<>
<h1>Form</h1>
<InnerForm key={resetHeuristicKey} resetForm={resetForm} />
</>
);
};
Example code (reset the MyFormComponent and it's state after submitted successfully):
function render() {
const [formkey, setFormkey] = useState( Date.now() )
return <>
<MyFormComponent key={formkey} handleSubmitted={()=>{
setFormkey( Date.now() )
}}/>
</>
}
Maybe you can use the method reset() of the form:
import { useRef } from 'react';
interface Props {
data: string;
}
function Demo(props: Props) {
const formRef = useRef<HTMLFormElement | null>(null);
function resetHandler() {
formRef.current?.reset();
}
return(
<form ref={formRef}>
<input defaultValue={props.data}/>
<button onClick={resetHandler}>reset</button>
</form>
);
}

Resources