React useImerativeHandle does not update value - javascript

See codesandbox here
I am trying to pass the state value of a child component up to its parent by using React's useImperativeHandle. However, it appears that my parent component is not receiving the updated state value of the child component when it console logs the child's component value; console.log(componentRef.current.state) always is logged as false.
Why is this not working and how can I accurately receive the mutated state value of my child component in my parent component by passing the necessary ref? Thanks!
index.tsx:
import React from "react";
import ReactDOM from "react-dom";
const Component = React.forwardRef((props, ref) => {
const [state, set] = React.useState(false);
React.useImperativeHandle(ref, () => ({
state
}));
const handleClick = () => {
set(prevState => !prevState);
};
return (
<>
<button type="button" onClick={handleClick}>
Click Me
</button>
<h1>{state ? "On" : "Off"}</h1>
</>
);
});
const App = () => {
const componentRef = React.useRef(null);
React.useEffect(() => {
console.log(componentRef.current.state);
}, [componentRef]);
return <Component ref={componentRef} />;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

If you just want that functionality you can use something like:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
const Component = props => {
const [state, set] = React.useState(false);
useEffect(() => props.callback(state), [state])
const handleClick = () => {
set(prevState => !prevState);
};
return (
<>
<button type="button" onClick={handleClick}>
Click Me
</button>
<h1 >{state ? "On" : "Off"}</h1>
</>
);
};
const App = () => {
return <Component callback={val => console.log(val)} />;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
But if you really need to use ref, just comment it so I'll remove this answer.

Related

React component not re-rendering when array prop changes

I want my App component to display keys every time keys changes. I'm doing this by passed keys as a prop of App:
import * as React from "react";
import { render } from "react-dom";
import { useState, useEffect } from "react"
let keys: string[] = [];
// This is what is supposed to happen in the real app
// document.addEventListener("keypress", (event) => {
// keys.push(event.key)
// });
setTimeout(() => {
keys.push('a');
}, 1000)
function App({ keys }: { keys: string[] }) {
let [keysState, setKeysState] = useState(keys)
useEffect(() => {
setKeysState(keys)
}, [keys])
return (
<div>
{keysState.map((key: string) => (
<li>{key}</li>
))}
</div>
);
}
const rootElement = document.getElementById("root");
render(<App keys={keys} />, rootElement);
However, App isn't re-rendering and displaying keys new value:
https://codesandbox.io/s/changing-props-on-react-root-component-forked-3mv0xf?file=/src/index.tsx
Why is this, and how to fix it?
Note: I tried: setKeysState([...keys, 'a']). That doesn't re-render App either.
Live code: https://codesandbox.io/s/changing-props-on-react-root-component-forked-3mv0xf?file=/src/index.tsx
All data that is dynamic needs to be managed by React. Put your event inside the component and update local state.
function App({ initialKeys }: { initialKeys: string[] }) {
const [keys, setKeys] = React.useState(initialKeys);
console.log(keys);
React.useEffect(() => {
const append = (e) => {
setKeys([...keys, e.key]);
};
document.addEventListener("keypress", append);
return () => {
document.removeEventListener("keypress", append);
};
}, [keys]);
return (
<div>
{keys.map((key: string, idx) => (
<li key={idx}>{key}</li>
))}
</div>
);
}
https://codesandbox.io/s/changing-props-on-react-root-component-forked-l869dd?file=/src/index.tsx
if you use the below strategy it works as you want it to work.
React cannot see state changes out of its built-in functions so it didn't track the change on your array which was out of its state scope
import * as React from "react";
import { render } from "react-dom";
import { useState, useEffect } from "react";
let keys: string[] = [];
function App(props: any) {
const [keys, oldKeysState] = useState(props.keys);
const [keysState, setKeysState] = useState(keys);
useEffect(() => {
setKeysState(keys);
}, [keys]);
// componentWillMount
useEffect(() => {
setTimeout(() => {
oldKeysState([...keys, "a"]);
}, 1000);
}, []);
return (
<div>
{keysState.map((key: string) => (
<li>{key}</li>
))}
<button onClick={() => setKeysState([...keysState, "+"])}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
render(<App keys={keys} />, rootElement);

Only conditionally re-render when context value updates

I'm am working on a large React application where performance is critical and unnecessary re-renders are costly.
I have the following example:
const CounterContext = React.createContext();
const CounterProvider = ({children}) => {
const [counterA, setCounterA] = React.useState(0);
const [counterB, setCounterB] = React.useState(0);
return (
<CounterContext.Provider value={{counterA, counterB}}>
{children}
<button onClick={() => setCounterA(counterA + 1)}>Counter A ++</button>
<button onClick={() => setCounterB(counterB + 1)}>Counter B ++</button>
<button onClick={() => {setCounterA(0); setCounterB(0)}}>reset</button>
</CounterContext.Provider>
)
}
const CounterA = () => {
const value = React.useContext(CounterContext);
console.log('CounterA re-render');
return <p>Counter A: {value.counterA}</p>;
}
const CounterB = () => {
const value = React.useContext(CounterContext);
console.log('CounterB re-render');
return <p>Counter B: {value.counterB}</p>;
};
const App = () => {
return (
<CounterProvider>
<CounterA />
<CounterB />
</CounterProvider>
)
};
jsfiddle: https://jsfiddle.net/mitchkman/k3hm0vfq/1/
When clicking the CounterA button, both CounterA and CounterB components will re-render. I'd like to only re-render CounterA, if the counterA property in value changes.
I'd also like to have the ability to have some form of flexibility for conditional re-rendering. This is a pseudocode of what I am trying to do:
const MyComponent = () => {
// Only re-render MyComponent if value.property equals 42
const value = useContext(MyContext, (value) => value.property === 42);
...
};
You may have to wrap the counterProvider to your App component. And use React.memo to achieve re-rendering only on value changes for that component.
I have done something like here in this sandbox
index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { CounterProvider } from "./AppContext";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<CounterProvider>
<App />
</CounterProvider>
</StrictMode>,
rootElement
);
Moved the counterContext to a separate file so that you can import and wrap your App component.
AppContext.js
import React, { useCallback } from "react";
const CounterContext = React.createContext();
export const CounterProvider = ({ children }) => {
const [counterA, setCounterA] = React.useState(0);
const [counterB, setCounterB] = React.useState(0);
const incrementA = () => {
setCounterA(counterA + 1);
};
const incrementB = () => {
setCounterB(counterB + 1);
};
return (
<CounterContext.Provider value={{ counterA, counterB }}>
{children}
<button onClick={incrementA}>Counter A ++</button>
<button onClick={incrementB}>Counter B ++</button>
<button
onClick={useCallback(() => {
setCounterA(0);
setCounterB(0);
}, [])}
>
reset
</button>
</CounterContext.Provider>
);
};
export default CounterContext;
Pass the counterA and counterB values to the Counter components as a props, so that React.memo can check that value before re-rendering the component.
App.js
import React from "react";
import CounterContext from "./AppContext";
const CounterA = React.memo(({ value }) => {
console.log("CounterA re-render");
return <p>Counter A: {value}</p>;
});
const CounterB = React.memo(({ value }) => {
console.log("CounterB re-render");
return <p>Counter B: {value}</p>;
});
const App = () => {
const value = React.useContext(CounterContext);
const counterA = value.counterA;
const counterB = value.counterB;
console.log(counterA);
return (
<>
<CounterA value={counterA} />
<CounterB value={counterB} />
</>
);
};
export default App;

Invoking a function from a child component in a parent component via callback

See codesandbox here
I want to use a callback in a child react component so that I can invoke a function from my child component in my parent component. However, when I handle the callback in my parent component, and try to set this child's function so that I can invoke this function later on when a button gets clicked, this function ends up getting invoked unexpectedly.
What I want to happen is that the sampleFunction to be invoked when the 'Invoke Sample Function' button is clicked, but instead, sampleFunction is invoked when the parent component is mounted (and the console is thus logged with 'foo'). How can I properly pass this callback function from the child to the parent? Thanks.
index.js:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
const Component = ({ callback }) => {
const [toggleStatus, setToggleStatus] = React.useState(false);
useEffect(() => {
callback({
toggleStatus,
sampleFunction: () => console.log("foo")
});
}, [callback, toggleStatus]);
const handleClick = () => {
setToggleStatus(prevState => !prevState);
};
return (
<>
<button type="button" onClick={handleClick}>
Click Me
</button>
<h1>{toggleStatus ? "On" : "Off"}</h1>
</>
);
};
const App = () => {
const [fn, setFn] = React.useState(null);
const handleCallback = props => {
setFn(props.sampleFunction);
};
return (
<>
<Component callback={handleCallback} />
<button type="button" onClick={fn}>
Invoke Sample Function
</button>
</>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Based on what this question covers and the usage you want, you should use useCallback (docs - more info) instead of useEffect. something like the following is kind of better implementation:
import React from "react";
import ReactDOM from "react-dom";
const Component = ({ callback }) => {
const [toggleStatus, setToggleStatus] = React.useState(false);
const handleClick = React.useCallback(() => {
setToggleStatus(prevState => !prevState);
callback({
toggleStatus,
sampleFunction: () => () => console.log("foo")
})
}, [callback, toggleStatus])
return (
<>
<button type="button" onClick={handleClick}>
Click Me
</button>
<h1>{toggleStatus ? "On" : "Off"}</h1>
</>
);
};
const App = () => {
const [fn, setFn] = React.useState(() => null);
const handleCallback = props => {
console.log(props)
setFn(props.sampleFunction);
};
return (
<>
<Component callback={handleCallback} />
<button type="button" onClick={fn}>
Invoke Sample Function
</button>
</>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You need to update your code as you are calling the callback in the initialization of your child component.
useEffect(() => {
// remove below call
callback({
toggleStatus,
sampleFunction: () => console.log("foo")
});
}, [callback, toggleStatus]);
See codesandbox here
Thanks to #StackedQ and #Anthony for the help. As Anthony mentioned, in order to avoid an endless loop, I had to add an extra condition to handleCallback:
index.js:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
const Component = ({ callback }) => {
const [toggleStatus, setToggleStatus] = React.useState(false);
useEffect(() => {
callback({
toggleStatus,
sampleFunction: () => () => setToggleStatus(prevState => !prevState)
});
}, [callback, toggleStatus]);
const handleClick = () => {
setToggleStatus(prevState => !prevState);
};
return (
<>
<button type="button" onClick={handleClick}>
Click Me
</button>
<h1>{toggleStatus ? "On" : "Off"}</h1>
</>
);
};
const App = () => {
const [fn, setFn] = React.useState(null);
const handleCallback = props => {
if (!fn) {
setFn(props.sampleFunction);
}
};
return (
<>
<Component callback={handleCallback} />
<button type="button" onClick={fn}>
Invoke Sample Function
</button>
</>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Getting state from child to parent

how would I get the state from the child so that the parent recognise the state from that child has changed?
const grandParent = () => (
<parent>
<child/>
</parent>
);
const child = () => {
const [isOpen, setOpen] = useState(false)
return (
<button onClick={()=>setOpen(!isOpen)}>Open</button>
)};
const grandParent = () => {
const [ isOpen, setOpen ] = useState(false)
return (
<parent>
<child onHandlerClick={ () => setOpen(!isOpen) }/>
</parent>
);
};
const child = (onHandlerClick) => {
// Note I removed the local state. If you need the state of the parent in the child you can pass it as props.
return (
<button onClick={ () => onHandlerClick() }>Open</button>
);
};
When you need to keep the state in the parent and modify it inside the children, no matter child state. You pass a handler in props from the parent where it's defined to modify the state. The child execute this handler.
This pattern is called state hoisting.
I think I would do something like that:
function GrandParent(){
return <Parent />
}
function Parent() {
const [isOpen, setOpen] = useState(false);
const handleToggle = useCallback(() => {
setOpen(!isOpen);
}, [isOpen, setOpen]);
return <Child handleToggle={handleToggle} />;
}
function Child(props) {
return <button onClick={() => props.handleToggle()}>Open</button>;
}
You can do the following using functional component. Write the Child component as below:
import React, {useEffect, useState} from 'react';
function Child(props) {
const {setStatus} = props;
const [isOpen, setOpen] = useState(false);
function clickHandler() {
setOpen(!isOpen);
setStatus(`changes is ${!isOpen}`);
}
return (
<div>
<button onClick={clickHandler}>Open</button>
</div>
);
}
export default Child;
Write the GrandParent component as below:
import React, {useEffect, useState} from 'react';
import Child from "./Child";
function GrandParent(props) {
function setStatus(status) {
console.log(status);
}
return (
<div>
<Child setStatus={setStatus}></Child>
</div>
);
}
export default GrandParent;
Use GrandParent component in App.js as below:
import React from "react";
import GrandParent from "./GrandParent";
class App extends React.Component {
render() {
return (
<GrandParent/>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
export default App;
You can add props to child and call onChange each time the state is changed
const grandParent = () => (
function handleChange() {}
<parent>
<child onChange={handleChange} />
</parent>
);
const child = (props) => {
const [isOpen, setOpen] = useState(false);
function onChange() {
setOpen(prevOpen => {
props.onChange(!prevOpen);
return !prevOpen;
});
}
return (
<button onClick={()=>setOpen(!isOpen)}>Open</button>
)};
You can do something like this:
const grandParent = () => {
const [isOpen, setOpen] = useState(false)
return (
<parent isOpen>
<child isOpen onChangeState={() => setOpen(!isOpen)} />
</parent>
)
}
const child = (props) => {
return (
<button
onClick={() => {
props.onChangeState()
}}>
Open
</button>
)
}
Explanation:
You manage the state in the grandParent component and passing it in the parent component (and also at the child if you need it).
The child has a prop which is called when the button is clicked and leads to the update of the grandParent state

How do I use context to share state between a class and a functional component in react?

I have a top level context Provider, followed by a Parent class component follow by a functional stateless Child.
I can update the my context value from the Child, but not from the parent, even though the value updates in the parent.
How can I update and share state between both components using context?
import React from "react";
import ReactDOM from "react-dom";
const Context = React.createContext();
const Provider = ({ children }) => {
const [value, setValue] = React.useState(0);
return (
<Context.Provider value={{ value, setValue }}>{children}</Context.Provider>
);
};
const Child = () => {
const { value, setValue } = React.useContext(Context);
return <div onClick={() => setValue(value + 1)}>Plus plus!!</div>;
};
class Parent extends React.Component {
render() {
const { value, setValue } = this.context;
return (
<div>
<div onPress={() => setValue(value - 1)}>MINUS MINUS!</div>
<div>{this.props.children}</div>
<h1>{value}</h1>
</div>
);
}
}
Parent.contextType = Context;
function App() {
return (
<Provider>
<Parent>
<Child />
</Parent>
</Provider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
https://codesandbox.io/s/thirsty-oskar-ocmxr
Change the "onPress" to "onClick" will work. I have tested it.

Categories