How to set initial state in 'useState' using a function? - reactjs

I am building a table component. It gets as a prop an object called content which holds records that are displayed as the table's content. The component has a state called 'currentRecord' which holds the id of the selected row (changes in onClick event on each row).
I want to set the first record's id to be the initial state using the 'useState'.
As an initial state argument for the 'useState' it has a function which return the key(which is the id) of the first record in the content prop. But it returns undefined. When console logging the return value of that function, it return an id.
Why does it return undefined when setting the initial state using a function?
I have tried setting the initial state using a string instead of a function, and it worked.
function getFirstOrderId(content:object): string {
return Object.keys(content)[0];
}
const Table: FunctionComponent<Props> = props => {
const { columnTitles, content, onRowClick } = props;
const [currentRecord, setCurrentRecord] = useState(getFirstOrderId(content));
useEffect(() => {
onRowClick(currentRecord);
}, [currentRecord]);
return (
<StyledTable>
<thead>
<tr>
{Object.values(columnTitles).map(fieldName => {
return <th>{fieldName}</th>;
})}
</tr>
</thead>
<StyledTBody>
{mapWithKeys((order: any, id: string) => {
return (
<StyledRow
key={id}
isSelected={id === currentRecord}
onClick={() => setCurrentRecord(id)}
onDoubleClick={() => window.open("/" + order)}
>
{Object.keys(columnTitles).map(fieldContent => {
return <td>{order[fieldContent]}</td>;
})}
</StyledRow>
);
}, content)}
</StyledTBody>
</StyledTable>
);
};
export default Table;

Put a function inside the useState hook and return the value.
const [value, setValue] = useState(() => ({key: "Param"}));
console.log(value) // output >> {key: "Param"}

This might work:
const [currentRecord, setCurrentRecord] = useState(null);
useEffect(()=>{ // This will run after 1st render
setCurrentRecord(getFirstOrderId(content)); // OPTION 1
setCurrentRecord(()=>{ // OPTION 2
return getFirstOrderId(content);
});
},[]);
You can set up a loading state to wait for the useEffect() to take place.

You can actually do lazy initialisation to state with a function. how ever you called the function and not passed in as a parameter, meaning you passed the returned value of the function as the initial value to use state.
You can check out the official explanation:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Related

react, map component, unexpected result

I am building Weather App, my idea is to save city name in database/localhost, place cities in useState(right now it's hard coded), iterate using map in first child component and display in second child component.
The problem is that 2nd child component outputs only one element (event though console.log prints both)
BTW when I change code in my editor and save, then another 'li' element appears
main component
const App = () => {
const [cities, setCities] = useState(['London', 'Berlin']);
return (
<div>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
export default App
first child component
const DisplayWeather = ({displayWeather}) => {
const [fetchData, setFetchData] = useState([]);
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData([...fetchData , data]);
})
}, [])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
export default DisplayWeather
second child component
const Weather = ({data}) => {
console.log(data) // it prints correctly both data
return (
<li>
{data.name} //display only one data
</li>
)
}
export default Weather
The Problem
The setFetchData hooks setter method is asynchronous by default, it doesn't give you the updated value of the state immediately after it is set.
When the weather result for the second city is returned and set to state, the current value fetchData at the time is still an empty array, so you're essentially spreading an empty array with the second weather result
Solution
Pass a callback to your setFetchData and get the current previous value of the state and then continue with your spread accordingly.
Like this 👇🏽
setFetchData((previousData) => [...previousData, data]);

My component is mutating its props when it shouldn't be

I have a component that grabs an array out of a prop from the parent and then sets it to a state. I then modify this array with the intent on sending a modified version of the prop back up to the parent.
I'm confused because as I modify the state in the app, I console log out the prop object and it's being modified simultaneously despite never being touched by the function.
Here's a simplified version of the code:
import React, { useEffect, useState } from 'react';
const ExampleComponent = ({ propObj }) => {
const [stateArr, setStateArr] = useState([{}]);
useEffect(() => {
setStateArr(propObj.arr);
}, [propObj]);
const handleStateArrChange = (e) => {
const updatedStateArr = [...stateArr];
updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value);
setStateArr(updatedStateArr);
}
console.log(stateArr, propObj.arr);
return (
<ul>
{stateArr.map((stateArrItem, index) => {
return (
<li key={`${stateArrItem._id}~${index}`}>
<label htmlFor={`${stateArrItem.name}~name`}>{stateArrItem.name}</label>
<input
name={`${stateArrItem.name}~name`}
id={`${stateArrItem._id}~input`}
type="number"
value={stateArrItem.keyValue}
data-index={index}
onChange={handleStateArrChange} />
</li>
)
})}
</ul>
);
};
export default ExampleComponent;
As far as I understand, propObj should never change based on this code. Somehow though, it's mirroring the component's stateArr updates. Feel like I've gone crazy.
propObj|stateArr in state is updated correctly and returns new array references, but you have neglected to also copy the elements you are updating. updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value); is a state mutation. Remember, each element is also a reference back to the original elements.
Use a functional state update and map the current state to the next state. When the index matches, also copy the element into a new object and update the property desired.
const handleStateArrChange = (e) => {
const { dataset: { index }, value } = e.target;
setStateArr(stateArr => stateArr.map((el, i) => index === i ? {
...el,
keyValue: value,
} : el));
}

React making an array of IDs from a checkboxes clicked

I'm trying to have a function run everytime I click a checkbox that adds the id for that it to an array so that I can then use that array to make an apollo call to a mutation in graphQL. What is currently happening is that every checkbox gets checked when I press any of them and arrays are consoled for every ID in the table. What am I doing wrong here?
const RouteLocationsSelector = (props) => {
const {count} = useMileState()
const dispatch = useMileDispatch()
const [checked, setChecked] = useState(false);
const handleClick = () => setChecked(!checked)
var locationArray = [];
function checkedLocations(id) {
if (!locationArray.includes(id)) {
return locationArray.push(id)
} else {
return locationArray.filter(function(e) { return e != id})
}
}
const { loading, error, data } = useQuery(GET_LOCATIONS);
if (loading) return <tbody><tr><td>Loading...</td><td></td><td></td></tr></tbody>;
if (error) return <tbody><tr><td>Errror, are you logged in?</td><td></td><td></td></tr></tbody>;
return data.locations.map(({ id, slug, gps }) => (
<tbody key={id}>
<tr>
<td>{id}</td>
<td>{slug}</td>
<td>{gps}</td>
<td>
<input type="checkbox"
onClick={handleClick}
onChange={checkedLocations(id), console.log(locationArray)}/>
</td>
</tr>
</tbody>
))
};
export default RouteLocationsSelector;
You should probably make the input box a fully controlled component by adding the checked property to it and setting it equal to the value of your local component state.
But also, since the check boxes are in a mapped array, you'll want to handle the state of whether or not they are in a checked state separately. So each checkbox should have it's own state stored in an array. So I imagine your useState hook should be an array of false values that get swapped to true for the index position of the checkbox that has changed.
Might also be a question of composition as well. You could abstract what is returned by the map function into it's own component so it can handle its own local state.
<input type="checkbox"
checked={checked}
onClick={handleClick}
onChange={() => { checkedLocations(id); console.log(locationArray)}}
/>
I also imagine you'd probably want to add an anonymous function to the onChange event so that it doesn't fire everytime.

is it possible to React.useState(() => {}) in React?

is it possible to use a function as my React Component's state ?
example code here:
// typescript
type OoopsFunction = () => void;
export function App() {
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => console.log('default ooops')
);
return (
<div>
<div onClick={ ooops }>
Show Ooops
</div>
<div onClick={() => {
setOoops(() => console.log('other ooops'))
}}>
change oops
</div>
</div>
)
}
but it doesn't works ... the defaultOoops will be invoked at very beginning, and when clicking change oops, the otrher ooops will be logged to console immediately not logging after clicking Show Ooops again.
why ?
is it possible for me to use a function as my component's state ?
or else React has its special ways to process such the function state ?
It is possible to set a function in state using hooks, but because state can be initialized and updated with a function that returns the initial state or the updated state, you need to supply a function that in turn returns the function you want to put in state.
const { useState } = React;
function App() {
const [ooops, setOoops] = useState(() => () => console.log("default ooops"));
return (
<div>
<button onClick={ooops}>Show Ooops</button>
<button
onClick={() => {
setOoops(() => () => console.log("other ooops"));
}}
>
change oops
</button>
</div>
);
}
ReactDOM.render(<App />, 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>
TL;DR
Yes, it is possible to use a function as the React Component's state. In order to do this, you must use a function returning your function in React.useState:
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => () => console.log('default ooops')
);
// or
const yourFunction = () => console.log('default ooops');
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => yourFunction
);
To update your function you also must use a function returning your function:
setOoops(() => () => console.log("other ooops"));
// or
const otherFunction = () => console.log("other ooops");
setOoops(() => otherFunction);
Detailed Answer
Notes about React.useState
The signature of useState in React with types is
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
It shows, that there are two ways to set the initial value in your state:
Provide the initial value as is (React.useState(0) - initial value 0),
Provide a function, which returns the initial value to be set (React.useState(() => 0) - initial value also 0).
Important to note: If you provide a function in React.useState, then this function is executed, when React.useState is executed and the returned value is stored as the initial state.
How to actually store functions
The problem here is if you want to store a function as state you can not provide it as initial state as is, because this results in the function being executed and its return value stored as state instead of the function itself. Therefore when you write
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => console.log('default ooops')
);
'default ooops' is logged immediately when React.useState is called and the return value (in this case undefined) is stored.
This can be avoided by providing a higher order function returning your function you want to store:
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => () => console.log('default ooops')
);
This way the outer function will be definitely executed when first running React.useState and its return value will be stored. Since this return value is now your required function, this function will be stored.
Notes about the state setter function
The state setter function's (here setOoops) signature is given as
Dispatch<SetStateAction<S>>
with
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
Like in React.useState there is also the possibility to update state with a value or a function returning the value. So in order to update state the higher order function from above has to be used as well.
The previous answers set me on the right track, but I needed to tweak my setState parameters slightly as I wanted to execute a function complete with parameters. The following worked for me!
const [handler, setHandler] = useState(() => {});
setHandler(() => () => enableScheduleById(scheduleId));

How to target a specific item to toggleClick on using React Hooks?

I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.
The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.
Heres my component.
const NavBar = props => {
const [links, setLinks] = useState(null);
const [notFound, setNotFound] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const fetchLinks = () => {
if (props.prismicCtx) {
// We are using the function to get a document by its uid
const data = props.prismicCtx.api.query([
Prismic.Predicates.at('document.tags', [`${config.source}`]),
Prismic.Predicates.at('document.type', 'navbar'),
]);
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
});
}
return null;
};
const checkForLinks = () => {
if (props.prismicCtx) {
fetchLinks(props);
} else {
setNotFound(true);
}
};
useEffect(() => {
checkForLinks();
});
const handleDropdownClick = e => {
e.preventDefault();
setIsOpen(!isOpen);
};
if (links) {
const linkname = links.map(item => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen}>
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
</Fragment>
) : (
<StyledNavBar.NavLink href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
);
});
// Render
return (
<StyledNavBar>
<StyledNavBar.NavContainer wide>
<StyledNavBar.NavWrapper row center>
<Logo />
{linkname}
</StyledNavBar.NavWrapper>
</StyledNavBar.NavContainer>
</StyledNavBar>
);
}
if (notFound) {
return <NotFound />;
}
return <h2>Loading Nav</h2>;
};
export default NavBar;
Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:
const [isOpen, setIsOpen] = useState({});
const handleDropdownClick = e => {
e.preventDefault();
const currentID = e.currentTarget.id;
const newIsOpenState = isOpen[id] = !isOpen[id];
setIsOpen(newIsOpenState);
};
And finally in your HTML:
const linkname = links.map((item, index) => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen[index]}>
// ... rest of your component
Note the new index variable in the .map function, which is used to identify which menu item you are clicking.
UPDATE:
One point that I was missing was the initialization, as mention in the other answer by #MattYao. Inside your load data, do this:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
setIsOpen(navlinks.map((link, index) => {index: false}));
});
Not related to your question, but you may want to consider skipping effects and including a key to your .map
I can see the first two useState hooks are working as expected. The problem is your 3rd useState() hook.
The issue is pretty obvious that you are referring the same state variable isOpen by a list of elements so they all have the same state. To fix the problems, I suggest the following way:
Instead of having one value of isOpen, you will need to initialise the state with an array or Map so you can refer each individual one:
const initialOpenState = [] // or using ES6 Map - new Map([]);
In your fetchLink function callback, initialise your isOpen state array values to be false. So you can put it here:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
// init your isOpen state here
navlinks.forEach(link => isOpen.push({ linkId: link.id, value: false })) //I suppose you can get an id or similar identifers
});
In your handleClick function, you have to target the link object and set it to true, instead of setting everything to true. You might need to use .find() to locate the link you are clicking:
handleClick = e => {
const currentOpenState = state;
const clickedLink = e.target.value // use your own identifier
currentOpenState[clickedLink].value = !currentOpenState[clickedLink].value;
setIsOpen(currentOpenState);
}
Update your component so the correct isOpen state is used:
<Dropdown toggleOpen={isOpen[item].value}> // replace this value
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
The above code may not work for you if you just copy & paste. But it should give you an idea how things should work together.

Resources