I have a state which is used to render some components conditionally, I have the following structure:
const SearchScreen = () => {
const [isSearchBarFocused, setIsSearchBarFocused] = React.useState(false);
return (
<>
<SearchBar onFocus={setIsSearchBarFocused} />
{isSearchBarFocuse ? <SearchSuggestionList isSearchBarFocused={isSearchBarFocused} setIsSearchBarFocused={setIsSearchBarFocused} /> : null}
</>
)
}
As you can see in the code above, the state seter is shared by both components, when the <SearchBar /> is focused the state changes to true and when an option is selected from the <SearchSuggestionList /> the state change to false. The problem with this approach is that I need to run some API calls and other inner state updates in <SearchSuggestionList /> component but this is unmounted due to the conditional rendering then some processes are not reached to be carried out.
then how would I execute code that is inside a component that can be unmounted?
At SearchScreencomponent: You need to add event onBlur for search-input, it will auto trigger when click outside search-input. And add two events onChange and value to get the enter value from search-input. Finally, don't check isSearchBarFocused variable outside SearchSuggestionList component.
export default function SearchScreen() {
const [isSearchBarFocused, setIsSearchBarFocused] = useState(false);
const [searchBarValue, setSearchBarValue] = useState("");
const handleChangeSearchValue = (e) => {
setSearchBarValue(e.target.value);
};
const handleBlurSearch = () => {
setIsSearchBarFocused(false);
};
const handleFocusSearch = () => {
setIsSearchBarFocused(true);
};
return (
<div className="App">
<div className="searchbar-wrapper">
<input
className="search-input"
type="text"
onFocus={handleFocusSearch}
onChange={handleChangeSearchValue}
value={searchBarValue}
onBlur={handleBlurSearch}
/>
<SearchSuggestionList
isSearchBarFocused={isSearchBarFocused}
searchBarValue={searchBarValue}
/>
</div>
</div>
);
}
At SearchSuggestionList component: You need to create isSearchBarFocused and searchBarValue for Props. The display atrribute will change none or block depend on isSearchBarFocused
.search-list {
display: none;
}
.search-list.active {
display: block;
}
and if searchBarValue changes value, useEffect will trigger and re-call API
import { useEffect, useState } from "react";
const MOCKUP_DATA = [
{ id: 1, value: "Clothes" },
{ id: 2, value: "Dress" },
{ id: 3, value: "T-shirt" }
];
const SearchSuggestionList = ({ searchBarValue, isSearchBarFocused }) => {
const [suggestionData, setSuggestionData] = useState([]);
useEffect(() => {
// useEffect will call API first time and re-call every time `searchBarValue` changed value.
console.log("re-call api");
setSuggestionData(MOCKUP_DATA);
}, [searchBarValue]);
const handleClick = (value) => {
console.log(value);
};
return (
<ul className={`search-list ${isSearchBarFocused ? "active" : ""}`}>
{suggestionData.map((item) => (
<li key={item.id} onMouseDown={() => handleClick(item.value)}>
{item.value}
</li>
))}
</ul>
);
};
export default SearchSuggestionList;
Warning: If you use onClick to replace onMouseDown, it will not work. Because onBlur event of search-input will trigger before onClick event of the items in SearchSuggestionList
You can demo it at this link: https://codesandbox.io/s/inspiring-cori-m1bsv0?file=/src/App.jsx
Related
I have a parent component with a handler function:
const folderRef = useRef();
const handleCollapseAllFolders = () => {
folderRef.current.handleCloseAllFolders();
};
In the parent, I'm rendering multiple items (folders):
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
name={folder.name}
content={folder.content}
id={folder.id}
ref={folderRef}
/>
))}
In the child component I'm using the useImperativeHandle hook to be able to access the child function in the parent:
const [isFolderOpen, setIsFolderOpen] = useState(false);
// Collapse all
useImperativeHandle(ref, () => ({
handleCloseAllFolders: () => setIsFolderOpen(false),
}));
The problem is, when clicking the button in the parent, it only collapses the last opened folder and not all of them.
Clicking this:
<IconButton
onClick={handleCollapseAllFolders}
>
<UnfoldLessIcon />
</IconButton>
Only collapses the last opened folder.
When clicking the button, I want to set the state of ALL opened folders to false not just the last opened one.
Any way to solve this problem?
You could create a "multi-ref" - ref object that stores an array of every rendered Folder component. Then, just iterate over every element and call the closing function.
export default function App() {
const ref = useRef([]);
const content = data.map(({ id }, idx) => (
<Folder key={id} ref={(el) => (ref.current[idx] = el)} />
));
return (
<div className="App">
<button
onClick={() => {
ref.current.forEach((el) => el.handleClose());
}}
>
Close all
</button>
{content}
</div>
);
}
Codesandbox: https://codesandbox.io/s/magical-cray-9ylred?file=/src/App.js
For each map you generate new object, they do not seem to share state. Try using context
You are only updating the state in one child component. You need to lift up the state.
Additionally, using the useImperativeHandle hook is a bit unnecessary here. Instead, you can simply pass a handler function to the child component.
In the parent:
const [isAllOpen, setAllOpen] = useState(false);
return (
// ...
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
isOpen={isAllOpen}
toggleAll={setAllOpen(!isAllOpen)}
// ...
/>
))}
)
In the child component:
const Child = ({ isOpen, toggleAll }) => {
const [isFolderOpen, setIsFolderOpen] = useState(false);
useEffect(() => {
setIsFolderOpen(isOpen);
}, [isOpen]);
return (
// ...
<IconButton
onClick={toggleAll}
>
<UnfoldLessIcon />
</IconButton>
)
}
CodeSandbox - https://codesandbox.io/s/distracted-taussig-n7e7q3?file=/src/App.js
As you can see, I iterate over the flaggers array with .map and render <div>true</div> or <div onClick={() => setToTrue(flag)}>false</div>. I assumed that if I were to click the second div, the refer property of that flag would be set to true and the component would re-render, making the div change to <div>true</div> but that doesn't seem to be the case.
In the setToTrue function I console.log the post object, and I can see that the refer property of the second flag has changed to true, but it is not shown in the UI.
import "./styles.css";
export default function App() {
const post = {
flaggers: [
{
refer: false
},
{
refer: false
}
]
}
const setToTrue = (flag) => {
flag.refer = true;
console.log(post)
}
return (
<div className="App">
{post.flaggers.map((flag) => (
<div>
{flag.refer ? <div>true</div> : <div onClick={() => setToTrue(flag)}>false</div>}
</div>
))}
</div>
);
}
Well, that's not how react you have to setState value to trigger the component to rerender which will cause to UI change try the below code it will work fine as I set a value on onClick that causes the component to rerender in short. I would suggest reading the documentation before going into the coding and know-how react works
React Documentation
export default function App() {
const [post, setPost] = React.useState([
{
refer: false,
},
{
refer: false,
},
]);
const setToTrue = (boolFlag, index) => {
const tempPost = [...post];
tempPost[index].refer = boolFlag;
setPost(tempPost);
};
return (
<div className='App'>
{post.map((flag, index) => (
<div key={`${index}flag`}>
{flag.refer ? <div onClick={() => setToTrue(false, index)}>true</div> : <div onClick={() => setToTrue(true, index)}>false</div>}
</div>
))}
</div>
);
}
I execute a component and this component fills the value profilePicRef once. However, I only want to display the Upload button when profilePicRef.current.preview is also no longer zero. However, I always get the error message TypeError: Cannot read property 'preview' of undefined. My question is, how can I now say if it is undefined, then don't take it into account and if it is not zero show it.
<PhotoFileHandler ref={profilePicRef} />
{
profilePicRef.current.preview !== null &&
<button className="button is-primary is-outlined" type="butto"
onClick={() => { onClickUpload(profilePicRef);
setActiveModal(''); }}>
<i className="fas fa-file-image"></i> Upload</button>
}
PhotoFileHandler
import React, { useState, forwardRef, useImperativeHandle, } from "react";
function PhotoFileHandler(props, ref) {
const [picName, setPicName] = useState(null);
const [preview, setPreview] = useState(null);
const [isPreview, setIsPreview] = useState(true);
const fileSelectedHandler = (event) => {
....
setPicName(event.target.files[0].name);
setPreview(reader.result);
setIsPreview(true);
}
}
catch (err) {
}
};
useImperativeHandle(ref, () => ({
isPreview,
preview,
picName,
checkProfilPicture() {
if (!preview) {
setIsPreview(false);
return false;
}
else {
setIsPreview(true);
return true;
}
},
getImage() {
return preview
},
removePreview() {
setIsPreview(false)
setPreview(null);
setPicName(null);
}
}),
);
return (
<div>
<input class="file-input" type="file" name="resume" accept=".png,.jpg,.jpeg, .jfif"
onChange={fileSelectedHandler} />
</div>
);
};
// eslint-disable-next-line
PhotoFileHandler = forwardRef(PhotoFileHandler);
export default PhotoFileHandler;
Is important to do with ref?
Alternative 0: Without Ref, Parent State
Alternative with state, you can see how it works here: https://codesandbox.io/s/admiring-gates-ctw6m?file=/src/App.js) :
Include one variable "file" const [selectedFile, setSelectedFile] = React.useState(null)
Send the setter function to PhotoFileHandler, I did something like: <PhotoFileHandler onImage={setSelectedFile} />
In PhotoFileHandler I did:
const fileSelectedHandler = (event) => {
props.onImage(event.target.files[0]);
}
Alternative 1: Force update with Parent State
If you need the ref, one workaround can be the trigger it when change it, like: https://codesandbox.io/s/sleepy-butterfly-iqmw3?file=/src/App.js,
Define in your parent component one state: const [wasUpdated, setWasUpdated] = React.useState("");
Include this in your validation to show the preview and upload button:
profilePicRef.current &&
profilePicRef.current.preview !== null &&
wasUpdated && (
<AllHtml />
)
)
Send the setter to the children <PhotoFileHandler ref={profilePicRef} onChange={setWasUpdated} />
In the children component you can trigger the event after updating preview.
useEffect(() => {
props.onChange(preview);
}, [preview]);
I am implementing a form which is generated using a Json. The Json is retrieved from API and then looping over the items I render the input elements. Here is the sample Json :
{
name: {
elementType: 'input',
label: 'Name',
elementConfig: {
type: 'text',
placeholder: 'Enter name'
},
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
}
Here is how I render the form :
render() {
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
const itemPerRow = 4;
const rows = [
...Array(Math.ceil(props.formElementsArray.length / itemPerRow))
];
const formElementRows = rows.map((row, idx) =>
props.formElementsArray.slice(
idx * itemPerRow,
idx * itemPerRow + itemPerRow
)
);
const content = formElementRows.map((row, idx) => (
<div className='row' key={idx}>
{row.map((formElement) => (
<div className='col-md-3' key={formElement.id}>
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
handleChange={(event) => props.changed(event, formElement.id)}
/>
</div>
))}
</div>
...
}
I am storing the form state in redux and on every input change , I update the state. Now the problem is everytime I update the state, the entire form is re-rendered again... Is there any way to optimise it in such a way that only the form element which got updated is re-rendered ?
Edit :
I have used React.memo in Input.js as :
export default React.memo(input);
My stateful Component is Pure component.
The Parent is class component.
Edit 2 :
Here is how I create formElementArray :
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
You can make content as a separate component like this.
And remove formElementsArray prop from parent component.
export default function Content() {
const formElementRows = useForElementRows();
formElementRows.map((row, idx) => (
<Input
formId={formElement.id}
handleChange={props.changed}
/>
)
}
Inside Input.js
const handleInputChange = useCallback((event) => {
handleChange(event, formId);
}, [formId, handleChange]);
<input handleChange={handleInputChange} />
export default React.memo(Input)
So you can memoize handleChange effectively. And it will allow us to prevent other <Input /> 's unnecessary renders.
By doing this forElementRows change will not cause any rerender for other components.
You could try a container, as TianYu stated; you are passing a new reference as change handler and that causes not only the component to re create jsx but also causes virtual DOM compare to fail and React will re render all inputs.
You can create a container for Input that is a pure component:
const InputContainer = React.memo(function InputContainer({
id,
elementType,
elementConfig,
value,
invalid,
shouldValidate,
touched,
label,
changed,
}) {
//create handler only on mount or when changed or id changes
const handleChange = React.useCallback(
(event) => changed(event, id),
[changed, id]
);
return (
<Input
elementType={elementType}
elementConfig={elementConfig}
value={value}
invalid={invalid}
shouldValidate={shouldValidate}
touched={touched}
label={label}
handleChange={handleChange}
/>
);
});
Render your InputContainer components:
{row.map((formElement) => (
<div className="col-md-3" key={formElement.id}>
<InputContainer
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
//re rendering depends on the parent if it re creates
// changed or not
changed={props.changed}
/>
</div>
))}
You have to follow some steps to stop re-rendering. To do that we have to use useMemo() hook.
First Inside Input.jsx memoize this component like the following.
export default React.memo(Input);
Then inside Content.jsx, memoize the value of elementConfig, shouldValidate, handleChange props. Because values of these props are object type (non-primitive/reference type). That's why every time you are passing these props, they are not equal to the value previously passed to that prop even their value is the same (memory location different).
const elementConfig = useMemo(() => formElement.config.elementConfig, [formElement]);
const shouldValidate = useMemo(() => formElement.config.validation, [formElement]);
const handleChange = useCallback((event) => props.changed(event, formElement.id), [formElement]);
return <..>
<Input
elementConfig={elementConfig }
shouldValidate={elementConfig}
handleChange={handleChange}
/>
<../>
As per my knowledge, this should work. Let me know whether it helps or not. Thanks, brother.
I'm running into the issue where I have created a functional component to render a dropdown menu, however I cannot update the initial state in the main App.JS. I'm not really sure how to update the state unless it is in the same component.
Here is a snippet of my App.js where I initialize the items array and call the functional component.
const items = [
{
id: 1,
value:'item1'
},
{
id: 2,
value:'item2'
},
{
id: 3,
value:'item3'
}
]
class App extends Component{
state = {
item: ''
}
...
render(){
return{
<ItemList title = "Select Item items= {items} />
And here is my functional componenet. Essentially a dropdown menu from a YouTube tutorial I watched (https://www.youtube.com/watch?v=t8JK5bVoVBw).
function ItemList ({title, items, multiSelect}) {
const [open, setOpen] = useState (false);
const [selection, setSelection] = useState([]);
const toggle =() =>setOpen(!open);
ItemList.handleClickOutside = ()=> setOpen(false);
function handleOnClick(item) {
if (!selection.some(current => current.id == item.id)){
if (!multiSelect){
setSelection([item])
}
else if (multiSelect) {
setSelection([...selection, item])
}
}
else{
let selectionAfterRemoval = selection;
selectionAfterRemoval = selectionAfterRemoval.filter(
current =>current.id == item.id
)
setSelection([...selectionAfterRemoval])
}
}
function itemSelected(item){
if (selection.find(current =>current.id == item.id)){
return true;
}
return false;
}
return (
<div className="dd-wraper">
<div tabIndex={0}
className="dd-header"
role="button"
onKeyPress={() => toggle(!open)}
onClick={() =>toggle(!open)}
onChange={(e) => this.setState({robot: e.target.value})}
>
<div className="dd-header_title">
<p className = "dd-header_title--bold">{title}</p>
</div>
<div className="dd-header_action">
<p>{open ? 'Close' : 'Open'}</p>
</div>
</div>
{open && (
<ul className ="dd-list">
{item.map(item =>(
<li className="dd-list-item" key={item.id}>
<button type ="button"
onClick={() => handleOnClick(item)}>
<span>{item.value}</span>
<span>{itemSelected(item) && 'Selected'}</span>
</button>
</li>
))}
</ul>
)}
</div>
)
}
const clickOutsideConfig ={
handleClickOutside: () => RobotList.handleClickOutside
}
I tried passing props and mutating the state in the functional component, but nothing gets changed. I suspect that it needs to be changed in the itemSelected function, but I'm not sure how. Any help would be greatly appreciated!
In a function component, you have the setters of the state variables. In your example, you can directly use setOpen(...) or setSelection(...). In case of a boolean state variable, you could just toggle by using setOpen(!open). See https://reactjs.org/docs/hooks-state.html (Chapter "Updating State") for further details.
So you need to do something like below . Here we are passing handleChange in parent Component as props to the child component and in Child Component we are calling the method as props.onChange
Parent Component:
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
value :''
}
}
handleChange = (newValue) => {
this.setState({ value: newValue });
}
render() {
return <Child value={this.state.value} onChange = {this.handleChange} />
}
}
Child Component:
function Child(props) {
function handleChange(event) {
// Here, we invoke the callback with the new value
props.onChange(event.target.value);
}
return <input value={props.value} onChange={handleChange} />
}