How to stop sibling components from Re-Rendering? - javascript

I'm trying to toggle the visibility of a component using another component like this:
const CompA = React.memo(({Vis})=>{
console.log('Rendered CompA');
return(<Pressable onPress={()=>{Vis[1](!Vis[0])}} style={...}></Pressable>);
})
const CompB = React.memo(({Vis})=>{
console.log('Rendered CompB');
return(<>{Vis[0]&&(<View style={...}></View>)}</>);
})
export default function App() {
const Vis=useState(()=>true);
return (<View style={styles.body}>
<CompA Vis={Vis}/>
<CompB Vis={Vis} />
</View>);
}
but every time I toggle the visibility even CompA gets re rendered
How do I only re render CompB everytime I change the visibility?

The array you receive from useState will never be === a previous array you received from useState; React creates a new array when you call useState each time. Just like [] === [] is always false, the check being done by memo on the props will always be false and it will re-render.
There are at least two ways to solve the problem:
Pass the component parts of that array instead, since the setter function is guaranteed to be stable, and only pass CompA the setter function since it doesn't need the flag (it can use the callback form of the setter).
If you really, really want to pass the array around instead, implement a custom memo callback for CompA that only looks at the second element in the array, and don't use the first element of the array in CompA. But I'd strongly recommend not doing that, the readability/semantics of it are very misleading/surprising.
Here's #1:
const CompA = React.memo(({setVisible}) => {
console.log("Rendered CompA");
return (<Pressable onPress={() => {setVisible(visible => !visible)}} style={...}></Pressable>);
});
const CompB = React.memo(({visible}) => {
console.log("Rendered CompB");
return (<>{visible && (<View style={...}></View>)}</>);
});
export default function App() {
const [visible, setVisible] = useState(true); // No need for the callback form here
return (<View style={styles.body}>
<CompA setVisible={setVisible} />
<CompB visible={visible} />
</View>);
}
Live Example:
const {useState} = React;
const CompA = React.memo(({setVisible}) => {
console.log("Rendered CompA");
return (<button onClick={() => {setVisible(visible => !visible)}}>pressable</button>);
});
const CompB = React.memo(({visible}) => {
console.log("Rendered CompB");
// Unfortunately, the version of Babel used by Stack
// Snippets is so old it doesn't understand shorthand
// fragment syntax
return (<React.Fragment>{visible && (<div>this is the View</div>)}</React.Fragment>);
});
function App() {
const [visible, setVisible] = useState(true); // No need for the callback form here
return (<div>
<CompA setVisible={setVisible} />
<CompB visible={visible} />
</div>);
}
ReactDOM.render(<App />, 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>

You need to pass particular props in components. isVisible and the function which change the value of isVisible. When passing function as p props to components and using React.memo you need to use useCallback in function hooks to prevent re-render stuff.
Read more about memo and usecallback
import React from "react";
import ReactDOM from "react-dom";
const { useState, useCallback, memo } = React;
const CompA = memo(({ handleChange }) => {
console.log("Rendered CompA");
return <Pressable onPress={handleChange}>Comp A</Pressable>;
});
const CompB = memo(({ isVisible }) => {
console.log("Rendered CompB", isVisible);
return isVisible && <View> CompB</View>;
});
function App() {
const [isVisible, setVisible] = useState(true);
const handleChange = useCallback(() => setVisible((isVisible) => !isVisible),[]);
return (
<View style={styles.body}>
<CompA handleChange={handleChange} />
<CompB isVisible={isVisible} />
</View>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
Live Example
const { useState, useCallback, memo } = React;
const CompA = memo(({ handleChange }) => {
console.log("Rendered CompA");
return <button onClick={handleChange}>Comp A</button>;
});
const CompB = memo(({ isVisible }) => {
console.log("Rendered CompB");
return isVisible && <div> CompB</div>;
});
function App() {
const [isVisible, setVisible] = useState(true);
const handleChange = useCallback(
() => setVisible((isVisible) => !isVisible),
[]
);
return (
<div>
<CompA handleChange={handleChange} />
<CompB isVisible={isVisible} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<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>
<div id="root"></div>

I have a solution but it doesn't look pretty:
let UniVis;
const CompA = React.memo(({Vis})=>{
console.log('Rendered CompA');
return(<Pressable onPress={()=>{UniVis[1](!UniVis[0])}} style={...}></Pressable>);
});
const CompB = React.memo(({Vis})=>{
console.log('Rendered CompB');
return(<>{Vis[0]&&(<View style={...}></View>)}</>);
})
export default function App() {
const Vis=useState(()=>true);
UniVis=Vis;
return (<View style={styles.body}>
<CompA />
<CompB Vis={Vis} />
</View>);
}
but it does work

Related

How to use React.forwardRef?

I want to pass ref to a child component with forwardRef, but current for the given ref is always null. Why?
Consider this simple example:
// Details.jsx
import { useRef, forwardRef } from 'react';
const Details = () => {
const usernameRef = useRef(null);
const InputClipboardButton = React.forwardRef((props, ref) => (
<ClipboardButton targetInputRef={ref} />
));
return (
<div>
<input type="text" ref={usernameRef} />
<InputClipboardButton ref={usernameRef} />
</div>
);
};
// ClipboardButton.jsx
const ClipboardButton = ({ targetInputRef }) => {
const copyToClipboard = () => {
console.log(targetInputRef);
}
<button onClick={copyToClipboard}>
Copy
</button>
};
When using forwardRef you must be mindful of the order of the parameters passed (props and ref):
You can define the component like so:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
Note how forwardRef takes two parameters:
The props
The ref being forwarded
You may also destructure the props value, as well as call the ref property a different name:
const FancyButton = React.forwardRef(({
btnType,
children
}, forwardedRef) => (
<button ref={forwardedRef} type={btnType} className="FancyButton">
{children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref} btnType="button">Click me!</FancyButton>;
Example adapted from React Docs
Clipboard Button is a component, so you must use forwardRef in this component as well
const ClipboardButton = forwardRef((props,ref) => {
const copyToClipboard = () => {
console.log(ref);
}
<button onClick={copyToClipboard}>
Copy
</button>
});

setState is not a function in react native

I am learning react native, have been getting this error setState is not a function in react native
I searched a lot but nothing was helpful enough.
I have created this simplified code to show the issue
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ Test1 }) => {
return (
<Button
onPress={() => {
Test1.setState(true);
}}
/>
);
};
const Test1 = () => {
const [state, setState] = useState(false);
if (state) {
return <Text>Test Working</Text>;
} else {
return <Text>Test Not Working</Text>;
}
};
const App = () => {
return (
<View>
<Test Test1={Test1} />
</View>
);
};
export default App;
this is the error: TypeError: Test1.setState is not a function
Please help me fix this.
States can be transferred to other component only as props. You need to call the Test1 component from the App and the Test component from the Test1, then you can pass the props to the Test from Test1. By this you don't need to move the state to other component. you can not pass any component as props and access state or methods from there. You can try this code:
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ setState}) => {
return (
<Button
onPress={() => {
setState(true);
}}
/>
);
};
const Test1 = () => {
const [state, setState] = useState(false);
if (state) {
return <Text>Test Working</Text>;
} else {
return <Test setState={setState} />;
}
};
const App = () => {
return (
<View>
<Test1 />
</View>
);
};
export default App;
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ setState }) => {
return (
<Button
onPress={() => {
setState(true);
}}
);
};
const Test1 = ({state}) => {
if (state) {
return <Text>Test Working</Text>;
} else {
return <Text>Test Not Working</Text>;
}
};
const App = () => {
const [state, setState] = useState(false);
return (
<View>
<Test1 state={state} />
<Test setState={setState} />
</View>
);
};
export default App;
There are two problems here.
Your Test1 component is not being used at all
Hooks, and local functions in general, may not be called outside of the component that they are declared on
If you want to manage some local state in your Test component, it needs to live in that component.

How to render a component when state and props are changed?

I need to show the props value (which is a simple string). Each time I get new search results, I'm sending in the props. At the very first render the props will always be undefined.
Edit:
Header.jsx
function Header() {
const [searchString, setString] = useState('');
const onChangHandler = (e) => {
setString(e.target.value);
};
const activeSearch = () => {
if (searchString.length > 0) {
<Home searchResults={searchString} />;
}
};
return (
<div>
<input
placeholder='Search here'
value={searchString}
onChange={(e) => onChangHandler(e)}
/>
<button onClick={activeSearch}>Search</button>
</header>
</div>
);
}
I searched for previous stackoverflow questions and reactjs.org but found no answer.
Home.jsx
import React, { useEffect, useState } from 'react';
function Home({ searchResults }) {
const [itemSearchResults, setResults] = useState([]);
const [previousValue, setPreviousValue] = useState();
// What function will re-render when the props are first defined or changed ?
useEffect(() => { // Doesn't work
setResults(searchResults);
}, [searchResults]);
return (
<div>
<h3>Home</h3>
<h1>{itemSearchResults}</h1>
</div>
);
}
export default Home;
App.js
function App() {
return (
<div className='App'>
<Header />
<Home />
<Footer />
</div>
);
}
I'm sending the input string only to check if the props will change at the child component ("Home").
Any experts here know what's the problem?
Why it doesn't work?
It's because the Home component is never used, even if it's included in the following snippet:
const activeSearch = () => {
if (searchString.length > 0) {
<Home searchResults={searchString} />;
}
};
The activeSearch function has a couple problems:
it is used as an event handler though it uses JSX (outside the render phase)
it doesn't return the JSX (would still fail inside the render phase)
JSX should only be used within the render phase of React's lifecycle. Any event handler exists outside this phase, so any JSX it might use won't end up in the final tree.
The data dictates what to render
That said, the solution is to use the state in order to know what to render during the render phase.
function Header() {
const [searchString, setString] = useState('');
const [showResults, setShowResults] = useState(false);
const onChangHandler = (e) => {
// to avoid fetching results for every character change.
setShowResults(false);
setString(e.target.value);
};
const activeSearch = () => setShowResults(searchString.length > 0);
return (
<div>
<input
value={searchString}
onChange={(e) => onChangHandler(e)}
/>
<button onClick={activeSearch}>Search</button>
{showResults && <Home query={searchString} />}
</div>
);
}
useEffect to trigger effects based on changing props
And then, the Home component can trigger a new search request to some service through useEffect.
function Home({ query }) {
const [results, setResults] = useState(null);
useEffect(() => {
let discardResult = false;
fetchResults(query).then((response) => !discardResult && setResults(response));
// This returned function will run before the query changes and on unmount.
return () => {
// Prevents a race-condition where the results from a previous slow
// request could override the loading state or the latest results from
// a faster request.
discardResult = true;
// Reset the results state whenever the query changes.
setResults(null);
}
}, [query]);
return results ? (
<ul>{results.map((result) => <li>{result}</li>))}</ul>
) : `Loading...`;
}
It's true that it's not optimal to sync some state with props through useEffect like the article highlights:
useEffect(() => {
setInternalState(externalState);
}, [externalState]);
...but in our case, we're not syncing state, we're literally triggering an effect (fetching results), the very reason why useEffect even exists.
const { useState, useEffect } = React;
const FAKE_DELAY = 5; // seconds
function Home({ query }) {
const [results, setResults] = useState(null);
useEffect(() => {
let queryChanged = false;
console.log('Fetch search results for', query);
setTimeout(() => {
if (queryChanged) {
console.log('Query changed since last fetch, results discarded for', query);
return;
}
setResults(['example', 'result', 'for', query])
}, FAKE_DELAY * 1000);
return () => {
// Prevent race-condition
queryChanged = true;
setResults(null);
};
}, [query]);
return (
<div>
{results ? (
<ul>
{results.map((result) => (
<li>{result}</li>
))}
</ul>
) : `Loading... (${FAKE_DELAY} seconds)`}
</div>
);
}
function Header() {
const [searchString, setString] = useState('');
const [showResults, setShowResults] = useState(false);
const onChangHandler = (e) => {
// to avoid fetching results for every character change.
setShowResults(false);
setString(e.target.value);
};
const activeSearch = () => setShowResults(searchString.length > 0);
return (
<div>
<input
placeholder='Search here'
value={searchString}
onChange={(e) => onChangHandler(e)}
/>
<button onClick={activeSearch}>Search</button>
{showResults && <Home query={searchString} />}
</div>
);
}
ReactDOM.render(<Header />, document.querySelector("#app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Better solution: Uncontrolled inputs
Another technique in your case would be to use an uncontrolled <input> by using a ref and only updating the search string on click of the button instead of on change of the input value.
function Header() {
const [searchString, setString] = useState('');
const inputRef = useRef();
const activeSearch = () => {
setString(inputRef.current.value);
}
return (
<div>
<input ref={inputRef} />
<button onClick={activeSearch}>Search</button>
{searchString.length > 0 && <Home query={searchString} />}
</div>
);
}
const { useState, useEffect, useRef } = React;
const FAKE_DELAY = 5; // seconds
function Home({ query }) {
const [results, setResults] = useState(null);
useEffect(() => {
let queryChanged = false;
console.log('Fetch search results for', query);
setTimeout(() => {
if (queryChanged) {
console.log('Query changed since last fetch, results discarded for', query);
return;
}
setResults(['example', 'result', 'for', query])
}, FAKE_DELAY * 1000);
return () => {
// Prevent race-condition
queryChanged = true;
setResults(null);
};
}, [query]);
return (
<div>
{results ? (
<ul>
{results.map((result) => (
<li>{result}</li>
))}
</ul>
) : `Loading... (${FAKE_DELAY} seconds)`}
</div>
);
}
function Header() {
const [searchString, setString] = useState('');
const inputRef = useRef();
const activeSearch = () => {
setString(inputRef.current.value);
}
return (
<div>
<input
placeholder='Search here'
ref={inputRef}
/>
<button onClick={activeSearch}>Search</button>
{searchString.length > 0 && <Home query={searchString} />}
</div>
);
}
ReactDOM.render(<Header />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Passing the state around
[The following line] brings the Home component inside the Header component, which makes duplicate
{searchString.length > 0 && <Home query={searchString} />}
In order to make the Header component reusable, the quickest way would be to lift the state up.
// No state needed in this component, we now receive
// a callback function instead.
function Header({ onSubmit }) {
const inputRef = useRef();
const activeSearch = () => {
// Uses the callback function instead of a state setter.
onSubmit(inputRef.current.value);
}
return (
<div>
<input ref={inputRef} />
<button onClick={activeSearch}>Search</button>
</div>
);
}
function App() {
// State lifted up to the parent (App) component.
const [searchString, setString] = useState('');
return (
<div className='App'>
<Header onSubmit={setString} />
{searchString.length > 0 && <Home query={searchString} />}
<Footer />
</div>
);
}
If that solution is still too limited, there are other ways to pass data around which would be off-topic to bring them all up in this answer, so I'll link some more information instead:
Thinking in React
What's the right way to pass form element state to sibling/parent elements?
Passing data to sibling components with react hooks?
Application State Management with React
How can I update the parent's state in React?
Top 5 React state management libraries in late 2020 (Redux, Mobx, Recoil, Akita, Hookstate)
if your props are passed as searchResults, then change the props to,
function Home({ searchResults}) {...}
and use
useEffect(() => { // code, function },[searchResults]) ).

Why does the component not re-render after callback?

Given the following two components, I expect the EntryList component to re-render after the state changes in the handleEnttryDelete after the button in EntryForm is clicked. Currently the state changes, but the UI isn't updating itself:
import React, { useState } from "react";
import Button from "#material-ui/core/Button";
import { render } from "#testing-library/react";
const EntryList = (props) => {
const [entryList, setEntryList] = useState(props.data);
const handleEntryDelete = (entry) => {
const newState = entryList.filter(function (el) {
return el._id != entry._id;
});
setEntryList(() => newState);
};
return (
<div>
{entryList.map((entry) => {
return (
<EntryForm entry={entry} handleEntryDelete={handleEntryDelete} />
);
})}
</div>
);
};
const EntryForm = (props) => {
const [entry, setEntry] = useState(props.entry);
return (
<div>
<Button onClick={() => props.handleEntryDelete(entry)}>
{entry._id}
</Button>
</div>
);
};
export default EntryList;
Your code probably works, but not as intended. You just have to use key while mapping arrays to components.
Therefore, React can distinguish which elements should not be touched during reconciliation when you delete one of the nodes
<div>
{entryList.map((entry) => {
return <EntryForm key={entry._id} entry={entry} handleEntryDelete={handleEntryDelete} />;
})}
</div>;

Should renderItem of FlatList be wrapped with react useCallback hook?

const Component = React.memo(props => {
const { url } = props;
const keyExtractor = useCallback(item => item.id, []);
const handleClick = useCallback(() => {
Linking.openURL(url);
}, [url]);
const renderItem = useCallback(({ item }) => {
return (
<TouchableOpacity onPress={handleClick}>
<Text>Test</Text>
</TouchableOpacity>
);
}, [handleClick]);
return (
<FlatList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
/>
);
});
Just like the code above.
If the renderItem function is wrapped with a useCallback hook. it must check if the handleClick function reference is changed because the handleClick function may be changed if url is different. and it looks a little strange.
I wonder if there is a significant difference in the performance of using useCallback to wrap renderItem.
What is the best practice of using hooks for renderItem functions? Thanks
Because you already use React.memo the Component will not be executed unless url changes, if url changes the useCallback will re create the functions anyway so you can leave them out provided that is the only prop that can change.
Here is some code demonstrating this, you can re render App as much as you want but it won't re render other components unless you change url.
const SubComponent = ({ onClick }) => {
console.log('sub component render');
return <button onClick={onClick}>log url</button>;
};
const PureComponent = React.memo(function PureComponent({
url,
}) {
console.log('pure component render', url);
//even if I do React.useCallback(fn,[url]) that would mean
// it creates onClick when url changes but it would already
// only create onClick when url changes because memo will
// memoize the component result and not execute PureComponent
// unless the url changes
const onClick = () => console.log('url is', url);
return <SubComponent onClick={onClick} />;
});
const App = () => {
const [, reRenderApp] = React.useState({});
const [url, setUrl] = React.useState(
new Date().toUTCString()
);
console.log('rendering App');
return (
<div>
<button onClick={() => reRenderApp({})}>
re render app
</button>
<button
onClick={() => setUrl(new Date().toUTCString())}
>
set url
</button>
<PureComponent url={url} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Categories

Resources