I'm looking for the easiest solution to pass data from a child component to his parent.
I've heard about using Context, pass trough properties or update props, but I don't know which one is the best solution.
I'm building an admin interface, with a PageComponent that contains a ChildComponent with a table where I can select multiple line. I want to send to my parent PageComponent the number of line I've selected in my ChildComponent.
Something like that :
PageComponent :
<div className="App">
<EnhancedTable />
<h2>count 0</h2>
(count should be updated from child)
</div>
ChildComponent :
const EnhancedTable = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Click me {count}
</button>
)
};
I'm sure it's a pretty simple thing to do, I don't want to use redux for that.
A common technique for these situations is to lift the state up to the first common ancestor of all the components that needs to use the state (i.e. the PageComponent in this case) and pass down the state and state-altering functions to the child components as props.
Example
const { useState } = React;
function PageComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1)
}
return (
<div className="App">
<ChildComponent onClick={increment} count={count} />
<h2>count {count}</h2>
(count should be updated from child)
</div>
);
}
const ChildComponent = ({ onClick, count }) => {
return (
<button onClick={onClick}>
Click me {count}
</button>
)
};
ReactDOM.render(<PageComponent />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
You can create a method in your parent component, pass it to child component and call it from props every time child's state changes, keeping the state in child component.
const EnhancedTable = ({ parentCallback }) => {
const [count, setCount] = useState(0);
return (
<button onClick={() => {
const newValue = count + 1;
setCount(newValue);
parentCallback(newValue);
}}>
Click me {count}
</button>
)
};
class PageComponent extends React.Component {
callback = (count) => {
// do something with value in parent component, like save to state
}
render() {
return (
<div className="App">
<EnhancedTable parentCallback={this.callback} />
<h2>count 0</h2>
(count should be updated from child)
</div>
)
}
}
To make things super simple you can actually share state setters to children and now they have the access to set the state of its parent.
example:
Assume there are 4 components as below,
function App() {
return (
<div className="App">
<GrandParent />
</div>
);
}
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
return (
<>
<div>{name}</div>
<Parent setName={setName} />
</>
);
};
const Parent = params => {
return (
<>
<button onClick={() => params.setName("i'm from Parent")}>
from Parent
</button>
<Child setName={params.setName} />
</>
);
};
const Child = params => {
return (
<>
<button onClick={() => params.setName("i'm from Child")}>
from Child
</button>
</>
);
};
so grandparent component has the actual state and by sharing the setter method (setName) to parent and child, they get the access to change the state of the grandparent.
you can find the working code in below sandbox,
https://codesandbox.io/embed/async-fire-kl197
IF we Have Parent Class Component and Child function component this is how we going to access child component useStates hooks value :--
class parent extends Component() {
constructor(props){
super(props)
this.ChildComponentRef = React.createRef()
}
render(){
console.log(' check child stateValue: ',
this.ChildComponentRef.current.info);
return (<> <ChildComponent ref={this.ChildComponentRef} /> </>)
}
}
Child Component we would create using
React.forwardRef((props, ref) => (<></>))
. and
useImperativeHandle(ref, createHandle, [deps])
to customizes the instance value that is exposed to parent components
const childComponent = React.forwardRef((props, ref) => {
const [info, setInfo] = useState("")
useEffect(() => {
axios.get("someUrl").then((data)=>setInfo(data))
})
useImperativeHandle(ref, () => {
return {
info: info
}
})
return (<> <h2> Child Component <h2> </>)
})
I had to do this in type script. The object-oriented aspect would need the dev to add this callback method as a field in the interface after inheriting from parent and the type of this prop would be Function. I found this cool!
Here's an another example of how we can pass state directly to the parent.
I modified a component example from react-select library which is a CreatableSelect component. The component was originally developed as class based component, I turned it into a functional component and changed state manipulation algorithm.
import React, {KeyboardEventHandler} from 'react';
import CreatableSelect from 'react-select/creatable';
import { ActionMeta, OnChangeValue } from 'react-select';
const MultiSelectTextInput = (props) => {
const components = {
DropdownIndicator: null,
};
interface Option {
readonly label: string;
readonly value: string;
}
const createOption = (label: string) => ({
label,
value: label,
});
const handleChange = (value: OnChangeValue<Option, true>, actionMeta: ActionMeta<Option>) => {
console.group('Value Changed');
console.log(value);
console.log(`action: ${actionMeta.action}`);
console.groupEnd();
props.setValue(value);
};
const handleInputChange = (inputValue: string) => {
props.setInputValue(inputValue);
};
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
if (!props.inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
console.group('Value Added');
console.log(props.value);
console.groupEnd();
props.setInputValue('');
props.setValue([...props.value, createOption(props.inputValue)])
event.preventDefault();
}
};
return (
<CreatableSelect
id={props.id}
instanceId={props.id}
className="w-100"
components={components}
inputValue={props.inputValue}
isClearable
isMulti
menuIsOpen={false}
onChange={handleChange}
onInputChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type something and press enter..."
value={props.value}
/>
);
};
export default MultiSelectTextInput;
I call it from the pages of my next js project like this
import MultiSelectTextInput from "../components/Form/MultiSelect/MultiSelectTextInput";
const NcciLite = () => {
const [value, setValue] = useState<any>([]);
const [inputValue, setInputValue] = useState<any>('');
return (
<React.Fragment>
....
<div className="d-inline-flex col-md-9">
<MultiSelectTextInput
id="codes"
value={value}
setValue={setValue}
inputValue={inputValue}
setInputValue={setInputValue}
/>
</div>
...
</React.Fragment>
);
};
As seen, the component modifies the page's (parent page's) state in which it is called.
I've had to deal with a similar issue, and found another approach, using an object to reference the states between different functions, and in the same file.
import React, { useState } from "react";
let myState = {};
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
myState.name=name;
myState.setName=setName;
return (
<>
<div>{name}</div>
<Parent />
</>
);
};
export default GrandParent;
const Parent = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Parent")}>
from Parent
</button>
<Child />
</>
);
};
const Child = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Child")}>
from Child
</button>
</>
);
};
Related
I have 2 components. In the parent component I have this:
const Demo = () => {
const [state, setState] = useState(true);
const onChange = useCallback(
(value: string) => {
console.log(value);
},
[],
);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<Bar searchHandler={onChangeSearchHandler} />
</div>
</div>
);
};
In the Bar component I have this:
const Bar = ({ searchHandler }) => {
console.log('bar');
return (
<div>
<input type="text" onChange={(value) => searchHandler(value.target.value)} />
</div>
);
};
Wrapping onChange with useCallback I expect to cache the function and when I click on <button onClick={() => setState(false)}>sds</button> I don't want to render Bar component, but it is triggered. Why Bar component is triggered and how to prevent this with useCallback?
This has nothing to do with the onChange function you're wrapping with useCallback. Bar gets re-rendered because you're changing the state through setState in its parent component. When you change the state in a component all its child components get re-rendered.
You can verify it yourself by trying this:
const Demo = () => {
const [state, setState] = useState(true);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<Bar />
</div>
</div>
);
};
const Bar = ({ searchHandler }) => {
console.log('bar');
return (
<div></div>
);
};
You'll see that the Bar gets re-rerender anyway.
If you want to skip re-rerendring any of the child components, you should memoize them using React.memo when applicable.
Also, you should familiarize yourself with how state in react works and how does it affect the nested components as this is a main concept.
The issue is that you haven't used React.memo on Bar component. The function useCallback works only if you use HOC from memo.
try this, in Bar component create this wrapped component:
const WrappedBar = React.memo(Bar);
and in parent component use this wrapped bar:
const Demo = () => {
const [state, setState] = useState(true);
const onChange = useCallback(
(value: string) => {
console.log(value);
},
[],
);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<WrappedBar searchHandler={onChangeSearchHandler} />
</div>
</div>
);
};
I'm dynamically generating children components of HOC parent (see below). I pass the props directly to one of children and set the prop in it. I expect to see child re-rendering on props change but it doesn't.
Is the code incorrect somewhere?
ParentComponent
...
const ParentComponent = ({children}) => {
const [state1, setState1] = useState(true);
...
const changeOpacity = event => setState1(!state1);
const renderChildren = React.useCallback(() => React.Children.toArray(children).map((child, index) => (
<div key={index} style={{opacity: `${state1 ? 0 : 1}`}}>
{child}
</div>
)), [state1]);
return (
<div>
<Button onClick={changeOpacity}>Toggle Opacity</Button>
{renderChildren()}
</div>
);
};
App.js
...
const App = () => {
const [prop1, setProp1] = useState(123);
return (
<ParentComponent>
<Child1 prop1={prop1} setProp1={setProp1} />
<Child2 />
</ParentComponent>
);
};
In your ParentComponent, the children are cloned and then used to render as a part of the return value from the renderChildren function. Since the logic to compute children is not run on change of props to children, your child component is not affected by a change in its prop.
You can add children dependency to useCallback and it will work fine.
const { useState, useCallback } = React;
const ParentComponent = ({children}) => {
const [state1, setState1] = useState(true);
const changeOpacity = event => setState1(!state1);
const renderChildren = useCallback(() => React.Children.map(children, (child, index) => (
<div key={index} style={{opacity: `${state1 ? 0 : 1}`}}>
{child}
</div>
)), [children, state1]);
return (
<div>
<button onClick={changeOpacity}>Toggle Opacity</button>
{renderChildren()}
</div>
);
};
const Child1 = ({prop1, setProp1}) => <div>{prop1} <button onClick={() => setProp1(234)}>Click</button></div>;
const Child2 = () => <div>Hello</div>
const App = () => {
const [prop1, setProp1] = useState(123);
return (
<ParentComponent>
<Child1 prop1={prop1} setProp1={setProp1} />
<Child2 />
</ParentComponent>
);
};
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app" />
Is there anything prevent you from the approach below;
const ParentComponent = ({children}) => {
const [state1, setState1] = useState(true);
...
const changeOpacity = event => setState1(!state1);
const renderChildren = useCallback(() => React.Children.toArray(children).map((child, index) => (
<div key={index}>
{child}
</div>
)), [children]);
return (
<div>
<Button onClick={changeOpacity}>Toggle Opacity</Button>
{state1 && renderChildren()}
</div>
);
};
Using Render props pattern I wanted to see if there was a way to make this work using the current setup. I have a Parent component that uses an Example component as a wrapper to render some children inside it. I wanted to pass off a ref from inside of Example to one of the children in the render prop. Is this possible ?
const Example = ({ children }) => {
const ref = useRef(null);
const [open, SetOpen] = useState(false);
const [controls] = useCustomAnimation(open, ref);
return (
<div>
{children({ ref })}
</div>
);
};
const Parent = () => {
return (
<div>
<Example>
{ref => {
<motion.div
ref={ref}
>
{console.log('ref= ', ref)}
....... more children
</motion.div>;
}}
</Example>
</div>
);
};
Yes, your current file is almost exactly correct. I setup an example, but here is the gist:
const Example = ({ children }) => {
const ref = useRef(null);
return <div>{children({ ref })}</div>;
};
const Parent = () => {
return (
<div>
<Example>
{({ ref }) => {
console.log(ref);
return <input type="text" ref={ref} />;
}}
</Example>
</div>
);
};
Note: You need to destructure the object you are passing into the children function.
I have a form page structured more or less as follows:
<Layout>
<Page>
<Content>
<Input />
<Map />
</Content>
</Page>
<Button />
</Layout>
The Map component should only be rendered once, as there is an animation that is triggered on render. That means that Content, Page and Layout should not re-render at all.
The Button inside Layout should be disabled when the Input is empty. The value of the Input is not controlled by Content, as a state change would cause a re-render of the Map.
I've tried a few different things (using refs, useImperativeHandle, etc) but none of the solutions feel very clean to me. What's the best way to go about connecting the state of the Input to the state of the Button, without changing the state of Layout, Page or Content? Keep in mind that this is a fairly small project and the codebase uses "modern" React practices (e.g. hooks), and doesn't have global state management like Redux, MobX, etc.
Here is an example (click here to play with it) that avoids re-render of Map. However, it re-renders other components because I pass children around. But if map is the heaviest, that should do the trick. To avoid rendering of other components you need to get rid of children prop but that most probably means you will need redux. You can also try to use context but I never worked with it so idk how it would affect rendering in general
import React, { useState, useRef, memo } from "react";
import "./styles.css";
const GenericComponent = memo(
({ name = "GenericComponent", className, children }) => {
const counter = useRef(0);
counter.current += 1;
return (
<div className={"GenericComponent " + className}>
<div className="Counter">
{name} rendered {counter.current} times
</div>
{children}
</div>
);
}
);
const Layout = memo(({ children }) => {
return (
<GenericComponent name="Layout" className="Layout">
{children}
</GenericComponent>
);
});
const Page = memo(({ children }) => {
return (
<GenericComponent name="Page" className="Page">
{children}
</GenericComponent>
);
});
const Content = memo(({ children }) => {
return (
<GenericComponent name="Content" className="Content">
{children}
</GenericComponent>
);
});
const Map = memo(({ children }) => {
return (
<GenericComponent name="Map" className="Map">
{children}
</GenericComponent>
);
});
const Input = ({ value, setValue }) => {
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
);
};
const Button = ({ disabled = false }) => {
return (
<button type="button" disabled={disabled}>
Button
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672</h1>
<Layout>
<Page>
<Content>
<Input value={value} setValue={setValue} />
<Map />
</Content>
</Page>
<Button disabled={value === ""} />
</Layout>
</div>
);
}
Update
Below is version with context that does not re-render components except input and button:
import React, { useState, useRef, memo, useContext } from "react";
import "./styles.css";
const ValueContext = React.createContext({
value: "",
setValue: () => {}
});
const Layout = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Layout rendered {counter.current} times</div>
<Page />
<Button />
</div>
);
});
const Page = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Page rendered {counter.current} times</div>
<Content />
</div>
);
});
const Content = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Content rendered {counter.current} times</div>
<Input />
<Map />
</div>
);
});
const Map = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Map rendered {counter.current} times</div>
</div>
);
});
const Input = () => {
const { value, setValue } = useContext(ValueContext);
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
);
};
const Button = () => {
const { value } = useContext(ValueContext);
return (
<button type="button" disabled={value === ""}>
Button
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672, method 2</h1>
<p>
Type something into input below to see how rendering counters{" "}
<s>update</s> stay the same
</p>
<ValueContext.Provider value={{ value, setValue }}>
<Layout />
</ValueContext.Provider>
</div>
);
}
Solutions rely on using memo to avoid rendering when parent re-renders and minimizing amount of properties passed to components. Ref's are used only for render counters
I have a sure way to solve it, but a little more complicated.
Use createContext and useContext to transfer data from layout to input. This way you can use a global state without using Redux. (redux also uses context by the way to distribute its data). Using context you can prevent property change in all the component between Layout and Imput.
I have a second easier option, but I'm not sure it works in this case. You can wrap Map to React.memo to prevent render if its property is not changed. It's quick to try and it may work.
UPDATE
I tried out React.memo on Map component. I modified Gennady's example. And it works just fine without context. You just pass the value and setValue to all component down the chain. You can pass all property easy like: <Content {...props} /> This is the easiest solution.
import React, { useState, useRef, memo } from "react";
import "./styles.css";
const Layout = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Layout rendered {counter.current} times</div>
<Page {...props} />
<Button {...props} />
</div>
);
};
const Page = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Page rendered {counter.current} times</div>
<Content {...props} />
</div>
);
};
const Content = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Content rendered {counter.current} times</div>
<Input {...props} />
<Map />
</div>
);
};
const Map = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Map rendered {counter.current} times</div>
</div>
);
});
const Input = ({ value, setValue }) => {
const counter = useRef(0);
counter.current += 1;
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<>
Input rendedred {counter.current} times{" "}
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
</>
);
};
const Button = ({ value }) => {
const counter = useRef(0);
counter.current += 1;
return (
<button type="button" disabled={value === ""}>
Button (rendered {counter.current} times)
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672, method 2</h1>
<p>
Type something into input below to see how rendering counters{" "}
<s>update</s> stay the same, except for input and button
</p>
<Layout value={value} setValue={setValue} />
</div>
);
}
https://codesandbox.io/s/weathered-wind-wif8b
I'm trying to implement MVVM in React (requirement from the class I'm taking). I'm using functional components for the view and have typescript classes for the ViewModel. My components do not re-render when a property is updated in the ViewModel though.
Here's a simple example for a page that should toggle between a login and sign up form. The setCurrentForm gets called correctly and the value in the ViewModel does update, but it doesn't change the View.
// AuthView.tsx
const AuthView: React.FC = () => {
const VM = new AuthViewModel();
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => VM.setCurrentForm()} />
} else {
form = <LoginForm setCurrentForm={() => VM.setCurrentForm()} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
// AuthViewModel.tsx
export default class AuthViewModel {
email: string = "";
password: string = "";
currentForm: FORMS = FORMS.SignUp;
setCurrentForm() {
console.log("Setting form in VM");
if (this.currentForm === FORMS.SignUp) {
console.log("Switching to login")
this.currentForm = FORMS.Login;
} else if (this.currentForm === FORMS.Login) {
console.log("Switching to signup")
this.currentForm = FORMS.SignUp;
}
}
}
I could force the re-render with a hook by updating an arbitrary value, but I think there's a better way to do this. What are your thoughts?
You might be missunderstanding how react components re-render, just because you change some property in another object it has no bearing on the component itself, even if it has taken a property from this object.
Hooks are directly connected to the reacts render mechanism and can trigger render cycles, as such you should use something like this:
const AuthView: React.FC = () => {
// if you don't put this in a state a new VM will be created when the component rerenders
const [VM] = useState(new AuthViewModel());
useEffect(() => {
// Maybe some handler code is needed?
}, VM.currentForm);
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => VM.setCurrentForm()} />
} else {
form = <LoginForm setCurrentForm={() => VM.setCurrentForm()} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
I've never tried to observe a nested property via a hook, so not 100% this works.
EDIT: it doesn't work, but it makes sense, the rendering call gets triggered when you actually call the set function of the useState hook, not really sure how to implement this pattern with hooks and without something like redux or mobx, but here is my best approach:
class AuthViewModel() {
constructor(public readonly currentForm = 'LOGIN');
public setCurrentForm = () => {
if(this.currentForm === 'LOGIN')
return new AuthViewModel('SIGNUP')
else
return new AuthViewModel(); // will default to login
}
}
and then the component
const AuthView: React.FC = () => {
// if you don't put this in a state a new VM will be created when the component rerenders
const [VM, setVM] = useState(new AuthViewModel());
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => setVM(VM.setCurrentForm())} />
} else {
form = <LoginForm setCurrentForm={() => setVM(VM.setCurrentForm())} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
What you have here doesn't feel very React. For starters, I've only rarely seen classes used outside of class-based components. I'm just going to spitball a different solution here that might not exactly match what you need, but hopefully gets you going in a correct direction.
const Authenticate: FC = props => {
const [mode, setMode] = useState<"login" | "create">("login");
return (
<div>
{mode === "login" && <Login onLogin={({email, password}) => {/*login handler logic*/}}/>}
{mode === "create" && <CreateAccount onCreate={({email, password}) => {/*create handler logic*/}}/>}
<button
disabled={mode === "login"}
onClick={() => setMode("login")}
>
login
</button>
<button
disabled={mode === "create"}
onClick={() => setMode("create")}
>
sign up
</button>
</div>
)
}
const Login: FC<{onLogin: ({email: string, password: string}) => any}> = props => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { onLogin } = props;
return (
<form onSubmit={() => onLogin({email, password})}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">Login</button>
</form>
);
}
const CreateAccount: FC<{onCreate: ({email: string, password: string}) => any}> = props => {
return (
<div>... similar to <Login/> ... </div>
)
}