React Hooks setState of an element in an array - javascript

How can I update a single element in a state array? Here is the code that I am currently using:
const Cars = props => {
const [cars, setCars] = React.useState(["Honda","Toyota","Dodge"])
const handleClick1 = () => { setCars[0]("Jeep") }
const handleClick2 = () => { setCars[1]("Jeep") }
const handleClick3 = () => { setCars[2]("Jeep") }
return (
<div>
<button onClick={handleClick1}>{cars[0]}</button>
<button onClick={handleClick2}>{cars[1]}</button>
<button onClick={handleClick3}>{cars[2]}</button>
</div>
)
};
When I click one of the rendered buttons, I get Uncaught TypeError: setCars[0] is not a function at handleClick1.
I know how to do this in a React Class, but how can I do this with React Hooks?

I suggest you map through your cars in order to render them - this is just overall a million times easier. From there you can apply an onClick handler to each button..
Furthermore, you should not mutate state like you are - always make a copy of state first, update the copy, then set your new state with the updated copy.
Edit: one thing that slipped my mind before was adding a key to each item when you are mapping over an array. This should be standard practice.
const { useState } = React;
const { render } = ReactDOM;
const Cars = props => {
const [cars, setCars] = useState(["Honda", "Toyota", "Dodge"]);
const updateCars = (value, index) => () => {
let carsCopy = [...cars];
carsCopy[index] = value;
setCars(carsCopy);
};
return (
<div>
{cars && cars.map((c, i) =>
<button key={`${i}_${c}`} onClick={updateCars("Jeep", i)}>{c}</button>
)}
<pre>{cars && JSON.stringify(cars, null, 2)}</pre>
</div>
);
};
render(<Cars />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>

I think you should correct these lines to spot the source of error
const handleClick1 = () => { setCars[0]("Jeep") }
into
const handleClick1 = () => { cars[0]="Jeep"; setCars(cars); }

Related

State not updating until I add or remove a console log

const BankSearch = ({ banks, searchCategory, setFilteredBanks }) => {
const [searchString, setSearchString] = useState();
const searchBanks = (search) => {
const filteredBanks = [];
banks.forEach((bank) => {
if (bank[searchCategory].toLowerCase().includes(search.toLowerCase())) {
console.log(bank[searchCategory].toLowerCase());
filteredBanks.push(bank);
}
});
setFilteredBanks(filteredBanks);
};
const debounceSearch = useCallback(_debounce(searchBanks, 500), []);
useEffect(() => {
if (searchString?.length) {
debounceSearch(searchString);
} else setFilteredBanks([]);
}, [searchString, searchCategory]);
const handleSearch = (e) => {
setSearchString(e.target.value);
};
return (
<div className='flex'>
<Input placeholder='Bank Search' onChange={handleSearch} />
</div>
);
};
export default BankSearch;
filteredBanks state is not updating
banks is a grandparent state which has a lot of objects, similar to that is filteredBanks whose set method is being called here which is setFilteredBanks
if I add a console log and save or remove it the state updates
Adding or removing the console statement and saving the file, renders the function again, the internal function's state is updated returned with the (setState) callback.
(#vnm)
Adding filteredBanks to your dependency array won't do much because it is part of the lexical scope of the function searchBanks
I'm not entirely sure of the total context of this BankSearch or what it should be. What I do see is that there are some antipatterns and missing dependencies.
Try this:
export default function BankSearch({ banks, searchCategory, setFilteredBanks }) {
const [searchString, setSearchString] = useState();
const searchBanks = useCallback(
search => {
const filteredBanks = [];
banks.forEach(bank => {
if (bank[searchCategory].toLowerCase().includes(search.toLowerCase())) {
filteredBanks.push(bank);
}
});
setFilteredBanks(filteredBanks);
},
[banks, searchCategory, setFilteredBanks]
);
const debounceSearch = useCallback(() => _debounce(searchBanks, 500), [searchBanks]);
useEffect(() => {
if (searchString?.length) {
debounceSearch(searchString);
} else setFilteredBanks([]);
}, [searchString, searchCategory, setFilteredBanks, debounceSearch]);
const handleSearch = e => {
setSearchString(e.target.value);
};
return (
<div className="flex">
<Input placeholder="Bank Search" onChange={handleSearch} />
</div>
)}
It feels like the component should be a faily simple search and filter and it seems overly complicated for what it needs to do.
Again, I don't know the full context, however, I'd look into the compont architecture/structuring of the app and state.

How callIndex works in react's useState under the hood?

I'm currently learning how React works under the hood and built a custom useState from scratch following the tutorials on Youtube.
But I'm having trouble understanding how callIndex is incremented and why do I need to set currentIndex variables which basically take the callIndex value.
The code works like this -
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
let callIndex = -1;
let stateValues = [];
const useState = (initValue) => {
callIndex++;
console.log("callIndex", callIndex);
const currentIdx = Number(callIndex);
if (stateValues[currentIdx] === undefined) {
stateValues[currentIdx] = initValue;
}
const setState = (newValue) => {
console.log("currentIdx in setState", currentIdx);
stateValues[callIndex] = newValue;
render();
};
return [stateValues[currentIdx], setState];
};
const App = () => {
const [countA, setCountA] = useState(1);
const [countB, setCountB] = useState(-1);
const [countC, setCountC] = useState(0);
return (
<div>
<div>
<h1>Count A: {countA}</h1>
<button onClick={() => setCountA(countA - 1)}>Subtract</button>
<button onClick={() => setCountA(countA + 1)}>Add</button>
</div>
<div>
<h1>Count B: {countB}</h1>
<button onClick={() => setCountB(countB - 1)}>Subtract</button>
<button onClick={() => setCountB(countB + 1)}>Add</button>
</div>
<div>
<h1>Count C: {countC}</h1>
<button onClick={() => setCountC(countC - 1)}>Subtract</button>
<button onClick={() => setCountC(countC + 1)}>Add</button>
</div>
</div>
);
};
function render() {
// callIndex = -1;
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
}
render();
Every time the useState function is called, the callIndex variables would be incremented by 1.
In this code, I called useState 3 times. So, the callIndex value would be 3.
The problem starts here - when I called the setState function, it'll first assign the newValue to the stateValues but in which index will it be changed.
Are there any articles or tutorials explaining how this happening?
The biggest issue is, you need to be able to distinguish between useState calls for different components, and different renders of the same component. You need this
<Comp1 />
const Comp1 = () => {
const [countA, setCountA] = useState(1);
const [countB, setCountB] = useState(-1);
to perform different logic than
<Comp1 />
<Comp2 />
const Comp1 = () => {
const [countA, setCountA] = useState(1);
const Comp2 = () => {
const [countB, setCountB] = useState(-1);
But the information available to you exposed by React with functional components is very limited, and won't allow you to do this easily. There is much more information used by (and available to) React's internals, which can distinguish things like the start of a component call.
If you want to "replicate" useState, I think the easiest way to do so would be to go outside of React completely, so that when "updating" the state, you can just call a function that re-populates a section of the DOM. (In React, you don't want to call ReactDOM.render on an app many times, and especially not on every render - that results in it being rebuilt from the ground up every time.)
Also, a small issue - when mounting, you should check if the state property exists on the array before assigning, not just if the value on that property is undefined, otherwise you'll lose the desired logic when nothing is passed as an initial value. This
if (stateValues[currentIdx] === undefined) {
stateValues[currentIdx] = initValue;
}
should be
if (!([currentIdx in stateValues)) {
stateValues[currentIdx] = initValue;
}
In your state setter, you also need to change
stateValues[callIndex] = newValue;
to
stateValues[currentIdx] = newValue;
For a vanilla JS demo of your tweaked code:
let callIndex = -1;
let stateValues = [];
const useState = (initValue) => {
callIndex++;
const currentIdx = Number(callIndex);
if (!(currentIdx in stateValues)) {
stateValues[currentIdx] = initValue;
}
const setState = (newValue) => {
stateValues[currentIdx] = newValue;
renderApp();
};
return [stateValues[currentIdx], setState];
};
const root = document.querySelector('.root');
const renderApp = () => {
callIndex = -1;
root.textContent = '';
root.appendChild(App());
};
const App = () => {
const [countA, setCountA] = useState(1);
const [countB, setCountB] = useState(-1);
const [countC, setCountC] = useState(0);
const div = document.createElement('div');
div.innerHTML = `
<div>
<h1>Count A: ${countA}</h1>
<button>Subtract</button>
<button>Add</button>
</div>
<div>
<h1>Count B: ${countB}</h1>
<button>Subtract</button>
<button>Add</button>
</div>
<div>
<h1>Count C: ${countC}</h1>
<button>Subtract</button>
<button>Add</button>
</div>`;
// this could be made less repetitive, but this is just for demo purposes
const buttons = div.querySelectorAll('button');
buttons[0].onclick = () => setCountA(countA - 1);
buttons[1].onclick = () => setCountA(countA + 1);
buttons[2].onclick = () => setCountB(countB - 1);
buttons[3].onclick = () => setCountB(countB + 1);
buttons[4].onclick = () => setCountC(countC - 1);
buttons[5].onclick = () => setCountC(countC + 1);
return div;
};
renderApp();
<div class="root"></div>

Why is calling setState twice causing a single state update?

This is probably a beginner React mistake but I want to call "addMessage" twice using "add2Messages", however it only registers once. I'm guessing this has something to do with how hooks work in React, how can I make this work?
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(messages.concat(message));
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
I'm using React 17.0.2
When a normal form of state update is used, React will batch the multiple setState calls into a single update and trigger one render to improve the performance.
Using a functional state update will solve this:
const addMessage = (message) => {
setMessages(prevMessages => [...prevMessages, message]);
};
const add2Messages = () => {
addMessage('Message1');
addMessage('Message2');
};
More about functional state update:
Functional state update is an alternative way to update the state. This works by passing a callback function that returns the updated state to setState.
React will call this callback function with the previous state.
A functional state update when you just want to increment the previous state by 1 looks like this:
setState((previousState) => previousState + 1)
The advantages are:
You get access to the previous state as a parameter. So when the new state depends on the previous state, the parameter is helpful as it solves the problem of stale state (something that you can encounter when you use normal state update to determine the next state as the state is updated asynchronously)
State updates will not get skipped.
Better memoization of handlers when using useCallback as the dependencies can be empty most of the time:
const addMessage = useCallback((message) => {
setMessages(prevMessages => [...prevMessages, message]);
}, []);
import React from "react";
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(messages => [...messages, message]);
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
This is because messages still refers to the original array. It will get the new array at the next re-render, which will occur after the execution of add2Messages.
Here are 2 solutions to solve your problem :
Use a function when calling setMessages
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(prevMessages => prevMessages.concat(message));
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
Modify addMessage to handle multiple messages
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (...messagesToAdd) => {
setMessages(prevMessages => prevMessages.concat(messagesToAdd));
// setMessages(messages.concat(messagesToAdd)); should also work
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => addMessage("Message1", "Message2")}>
Add 2 messages
</button>
</div>
);
}
Changing addMessage function as below will make your code work as expected
const addMessage = (message) => {
setMessages(messages => messages.concat(message));
};
Your code didn't work because in case of synchronous event handlers(add2Messages) react will do only one batch update of state instead of updating state after every setState calls. Which is why when second addMessage was called here, the messages state variable will have [] only.
const addMessage = (message) => {
setMessages(messages.concat(message));
};
const add2Messages = () => {
addMessage('Message1'); // -> [].concat("Message1") = Message1
addMessage('Message2'); // -> [].concat("Message2") = Message2
};
So if you want to alter the state value based on previous state value(especially before re-rendering), you can make use of functional updates.

retrieve parent component ref for a hook

I've found myself needing to retrieve the element ref for every parent component that my hook, useExample, is used in. However, I'm stumped as to how I might be able to retrieve something like this or how to even check if there is an element to target?
Usually I would just do something a little "hacky" in a functional component like so:
const Example = WrappedComponent => {
const ref = createRef();
return <WrappedComponent ref={ref} />;
};
However, due to it being a hook and returning information and not a component, I can't target any component, and thus I'm very stumped.
My current code:
const useExample = () => {
const [stateValue, setStateValue] = useState("example");
useEffect(() => {
// Run some code...
}, []);
return stateValue;
};
const Component = () => {
const data = useExample();
return (
<div> /* <--- How do I gain access to this element */
<span>{ data }</span>
</div>
);
};
I could probably pass a created ref which has been attached to the parent div as a parameter to useExample, however this feels cheap and hacky, and I feel there should be a much easier solution.
In the ideal world something like this would be amazing:
const ref = React.getParentRef();
Apologies if there is an obvious answer in the documentation, I'm very new to React and am unsure of the correct question to be asking or what to be looking for in order to find it in the docs.
You can return the ref from the hook
const useExample = () => {
const myRef = React.useRef(null)
const [stateValue, setStateValue] = useState("example");
useEffect(() => {
// Run some code...
}, []);
return [myRef , stateValue];
};
const Component = () => {
const [myRef , data] = useExample();
return (
<div ref={myRef}> /* <--- How do I gain access to this element */
<span>{ data }</span>
</div>
);
};
If data can be a component:
const useExample = () => {
const myRef = React.useRef(null);
const [stateValue, setStateValue] = React.useState("example");
React.useEffect(() => {
const parent = myRef?.current?.parentNode;
console.log(parent);
}, []);
return <div ref={myRef}>{stateValue}</div>;
};
const Component = () => {
const data = useExample();
return (
<div>
<span>{data}</span>
</div>
);
};
export default function App() {
return <Component />;
}
But then you have to access the parent node from the ref, I believe this may cause problems as a component is being returned, and its anti pattern

How to update the UI when the value of the array changes

I am displaying the name of each skid by iterating the skidList array. I have also provided a button "copy" which calls insertSkid when clicked and "delete" which calls deleteSkid. In insertSkid I am updating the skidList array. However, the Modal.Body doesn't update the skid names on changes to the skidList array.
Here's my code.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
let newSkid = {};
newSkid[name] = "PEN";
let newSkidList = [];
newSkidList.push(newSkid);
setSkidList(newSkidList);
const insertSkid = skid => {
let newSkidList = skidList;
newSkidList.push(skid);
setSkidList(newSkidList);
console.log("Added New Skid" + skidList.length);
};
const deleteSkid = (index) => { setSkidList([...skidList.splice(index, 1)]); }
return (
<Modal
backdrop="static"
show={true}
centered
dialogClassName={"createnewskid-modal"}
>
<Modal.Body>
{skidList.flatMap((skid, index) => (
<div>
{skid[name]}
<Button onClick={insertSkid.bind(this,skid)}>copy</Button>
<Button onClick={()=> deleteSkid(index)}>delete</Button>
<Divider />
</div>
))}
</Modal.Body>
</Modal>
)
}
There are several issues with the code your presenting. As previously stated, the useEffect() hook would be useful here to handle the initialization of the skid array:
useEffect(()=>{
let newSkid = {name: "PEN"};
setSkidList([...skidList, newSkid])
},[])
// The empty array second param tells the component to only execute the above code once on mount
Note: The spread operator (...) is used here to generate a new list based on skidList to which we are appending newSkid.
Next, you should use the same method just described to update your list in the insertSkid function:
const insertSkid = (skid) =>{
setSkidList([...skidList, skid])
}
Finally, I would suggest instead of binding you function, use a anonymous function inside the onclick prop:
onClick = {() => insertSkid(skid)}
if you want to setSkidList(newSkidList), do it inside useEffect.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
useEffect(()=> {
let newSkid = {};
newSkid[name] = "PEN";
setSkidList([newSkid]);
}, [])
//...
}
Call setSkidList out of the useEffect block will cause multiple state updates
It because an array is available by link, and for React skidList after push was not change. Try:
const insertSkid = useCallback((skid) => {
setSkidList([...skidList, skird]);
}, [skidList]);
you can use useEffect for inital data load, and for update you can use prev to get last state value and update yours and do the return, its not needed you need to add more variable assignment.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
useEffect(() => {
const newSkid = {[name] : "PEN"};
setSkidList([newSkid]);
}, [])
const insertSkid = skid => {
setSkidList(prev => [...prev, skid]);
};
return (
<div>
{skidList.flatMap((skid, index) => (
<div>
{skid[name]}
<button onClick={insertSkid.bind(this,skid)}>copy</button>
<hr />
</div>
))}
</div>
)
}

Categories

Resources