How to check when useState variable becomes available - javascript

I have useState variable that gets set based on a Promise that's resolved, how can I access the variable once it been setup
at the moment to get the correct values I have to use a setTimeout function, just wondering if there was a better way of doing that.
const FlagScreen = ({ t, i18n, history }) => {
const [flagAvailability, setFlagAvailability] = useState([]);
const [showFlags, setShowFlags] = useState([]);
useEffect(() => {
let flagsAvailable = [];
for (let [key, value] of Object.entries(flags)) {
if (key.indexOf(i18n.language) !== -1) {
for (const v of value) {
checkForAvailableAgent(`sales_${v}`, LINK_TO_STUDIO, SERVICE_ID)
.then(res => {
flagsAvailable[v] = res;
// Sets the flags availability i.e de: false, en: true
setFlagAvailability(flagsAvailable);
})
.catch(error => {
console.log("an error happened.");
});
}
}
}
}, [i18n.language]);
useEffect(() => {
//the value of flagAvailability is not available yet, I have to set a timeout function for
// 3 seconds for it to be available
console.log("flag availability: ", Object.entries(flagAvailability));
for (let [k, v] of Object.entries(flagAvailability)) {
console.log("key is: ", k);
if (v === true) {
setShowFlags(k);
}
}
}, [flagAvailability]);
}
<Container className="h-100">
<Row className="h-45 mt-5 text-center">
{ALL_STUDIOS_FLAGS.filter(item => {
return showFlags.includes(item);
}).map((item, index) => (
<Col key={index}>
<img
src={require(`../assets/flags/${item}.png`)}
alt={`${item} flag`}
/>
<span>{item.toUpperCase()}</span>
</Col>
))}
</Row>
any help would be appreicated,

The setFlagAvailability(flagsAvailable); in the first useEffect will only update the value of flagAvailability in second render
the second useEffect in the first render will only get the initial value of flagAvailability ( I know it's like a circle)
Every render has its own useEffect and useState
One of the solutions is to skip the first render in setShowFlags useEffect by creating a useRef flag like this
const ref = useRef(false)
const [flagAvailability, setFlagAvailability] = useState(true);
const [showFlags, setShowFlags] = useState(true);
useEffect(()=> {
if(ref.current){
...
setShowFlags();
ref.current = false;
}
},[flagAvailability])
useEffect(() => {
...
setFlagAvailability();
ref.current = true
},[i18n.language])
or you can simple change flagAvailability as useRef cause the refvalue will always be the new value.
const flagRef = useRef([]])
const [showFlags, setShowFlags] = useState(true);
useEffect(() => {
flagRef.current = flagsAvailable
},[i18n.language])
useEffect(()=> {
for (let [k, v] of Object.entries(flagRef.current)) {
setShowFlags();
}
},[showFlags])

Related

React JS Filters being overwritten by setState data in useEffect

I have a table where I'm setting data inside a useEffect from an api. My filter logic iterates through the "rows" variable which is being set inside this useEffect. However, every-time the user searches via an input which has an onChange event the useEffect setRows I believe is setting the data over and over again.
What would be a better way to set the data so it doesn't conflict with my filtering logic?
//State
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
//useEffect to setData
useEffect(() => {
//setDocuments from claimStore when component mounts
setDocuments(claimsStore.getIncomingDocuments());
//setRows from documents when component mounts
setRows(
documents.map((document) =>
createData(
document.documentAuthor ?? '',
document.documentMetadataId.toLocaleString(),
document.documentMetadataId.toLocaleString(),
document.documentName ?? '',
document.documentSource ?? '',
document.documentType,
document.featureId ?? '',
document.mimeType,
document.uploadDateTime,
),
),
);
}, [claimsStore, documents]);
//Filter logic that updates rows as user input values captured
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
//Filter documents based on user input
const updatedList = rows.filter((document) => {
return (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
);
});
//Trigger render with updated values
setRows(updatedList);
};
Use of filterBySearch:
<TableCell align={'center'} className={classes.tableCell}>
<input
value={filters.featureId}
onChange={(e) => filterBySearch(e)}
name="featureId"
className={classes.inputCell}
/>
</TableCell>
This is one of the things useMemo is good for: Have an array of filtered rows, that you update as necessary when rows or filters changes:
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
// ...
const filteredRows = useMemo(
() => rows.filter((document) => (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
)),
[rows, filters]
);
Then display filteredRows, not rows.
With that change, filterBySearch just sets the filter, it doesn't actually do the filtering:
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
};
useMemo will only call your callback when either rows or filters changes; otherwise, it'll just return the previous filtered array.
Here's a simplified demo — it shows words filtered by whatever you type in the filter, and randomly adds a word once every couple of seconds (this demonstrates that the filtering is repeated when the filter changes or when the rows change):
const { useState, useEffect, useRef, useMemo } = React;
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
return (
<div>
<div>
Filter: <input type="text" value={filter} onChange={filterChange} />
</div>
Rows - showing {filteredRows.length} of {rows.length} total:
<div>
{filteredRows.map((row) => (
<div key={row.id}>{row.value}</div>
))}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
React's documentation says that useMemo is just for performance enhancement, it isn't a semantic guarantee (basically, React may call your callback even when nothing has actually changed). If you want a semantic guarantee, you can do it with a ref. You can even wrap that up into a hook that provides the semantic guarantee — I call it useHardMemo:
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
Live Example:
const { useState, useEffect, useRef, createElement } = React;
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useHardMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
// I'm using `createElement` because I had to turn off SO's hopelessly outdated Babel because
// I wanted to be able to use optional chaining and such; so I couldn't use JSX.
// return (
// <div>
// <div>
// Filter: <input type="text" value={filter} onChange={filterChange} />
// </div>
// Rows - showing {filteredRows.length} of {rows.length} total:
// <div>
// {filteredRows.map((row) => (
// <div key={row.id}>{row.value}</div>
// ))}
// </div>
// </div>
// );
return createElement(
"div",
null,
createElement(
"div",
null,
"Filter: ",
createElement("input", { type: "text", value: filter, onChange: filterChange })
),
`Rows - showing ${filteredRows.length} of ${rows.length} total:`,
createElement(
"div",
null,
filteredRows.map((row) => createElement("div", { key: row.id }, row.value))
)
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(createElement(Example));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

How to map an array of objects in React useReducer function

I have an array of objects in my React state. I want to be able to map through them, find the one I need to update and update its value field. The body of my request being sent to the server should look like:
{ name: "nameOfInput", value:"theUserSetValue" type: "typeOfInput" }
What I thought would be simple is causing me some heartache. My reducer function calls, and I hit the "I AM RUNNING" log where it then jumps over my map and simply returns my state (which is empty). Please note that I NEVER see the "I SHOULD RETURN SOMETHING BUT I DONT" log.
NOTE: I have learned that I could be simply handingling this with useState
function Form(props) {
const title = props.title;
const paragraph = props.paragraph;
const formBlocks = props.blocks.formBlocks
const submitEndpoint = props.blocks.submitEndpoint || "";
const action = props.blocks.action || "POST";
const formReducer = (state, e) => {
console.log("I AM RUNNING")
state.map((obj) => {
console.log("I SHOULD RETURN SOMETHING BUT I DONT")
if (obj.name === e.target.name) {
console.log("OBJ EXISTS", obj)
return {...obj, [e.target.name]:obj.value}
} else {
console.log("NO MATCH", obj)
return obj
}
});
return state
}
const [formData, setFormData] = useReducer(formReducer, []);
const [isSubmitting, setIsSubmitting] = useState(false);
=====================================================================
Where I am calling my reducer from:
<div className="form-block-wrapper">
{formBlocks.map((block, i) => {
return <FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={setFormData}
/>
})}
</div>
Issues
When using the useReducer hook you should dispatch actions to effect changes to the state. The reducer function should handle the different cases. From what I see of the code snippet it's not clear if you even need to use the useReducer hook.
When mapping an array not only do you need to return a value for each iterated element, but you also need to return the new array.
Solution
Using useReducer
const formReducer = (state, action) => {
switch(action.type) {
case "UPDATE":
const { name, value } = action.payload;
return state.map((obj) => obj.name === name
? { ...obj, [name]: value }
: obj
);
default:
return state;
}
};
...
const [formData, dispatch] = useReducer(formReducer, []);
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={e => dispatch({
type: "UPDATE",
payload: {...e.target}
})}
/>
);
})}
Using useState
const [formData, setFormData] = useState([]);
...
const changeHandler = e => {
const { name, value } = e.target;
setFormData(data => data.map(obj => obj.name === name
? { ...obj, [name]: value }
: obj
));
};
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={changeHandler}
/>
);
})}
I have come to understand my problem much better now and I'll update my question to reflect this.
As the user interacted with an input I needed to figure out if they had interacted with it before
If they did interact with it before, I needed to find that interaction in the state[] and update the value as required
If they didn't I needed to add an entirely new object to my forms state[]
I wrote two new functions, an AddObjectToArray function and an UpdateObjectInArray function to serve these purposes.
const handleFormInputChange = (e) => {
const { name, value, type } = e.target;
const addObjectToArray = (obj) => {
console.log("OBJECT TO BE ADDED TO ARRAY:", obj)
setFormData(currentArray => ([...currentArray, obj]))
}
const updateObjectInArray = () => {
const updatedObject = formData.map(obj => {
if (obj.name === name) {
//If the name matches, Update the value of the input
return ({...obj, value:value})
}
else {
//if no match just return the object as is
return obj
}
})
setFormData(updatedObject)
}
//Check if the user has already interacted with this input
if (formData.find(input => input.name === name)) {
updateObjectInArray()
}
else {
addObjectToArray({name, value, type})
}
}
I could get more complicated with this now and begin to write custom hooks that take a setState function as a callback and the data to be handled.

Make React JS await for a async func to complete before running

I'm trying to make react not load until after an axios get requests finishes. I'm pretty rough on react all around, so sorry in advance.
I'm getting an array of objects
const { dogBreedsTest } = useApplicationData()
And I need it to be the default value of one of my states
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest);
However, I'm getting an error that my value is coming up as null on the first iteration of my app starting. How can I ensure that my value has completed my request before my app tries to use it?
Here is how I am getting the data for useApplicationData()
const [dogBreedsTest, setDogBreeds] = useState(null);
const getDogBreeds = async () => {
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
setDogBreeds
}
And I am importing into my app and using:
import useApplicationData from "./hooks/useApplicationData";
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest[0]);
const [breedList1, updateBreedList1] = useState(dogBreedsTest[0])
function handleOnDragEnd(result) {
if (!result.destination) return;
const items = Array.from(dogBreeds);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
for (const [index, item] of items.entries()) {
item['rank'] = index + 1
}
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0])
}
return (
<div className="flex-container">
<div className="App-header">
<h1>Dog Breeds 1</h1>
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="characters">
{(provided) => (
<ul className="dogBreeds" {...provided.droppableProps} ref={provided.innerRef}>
{breedList1?.map(({id, name, rank}, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</div>
)
error: TypeError: Cannot read property 'map' of null
(I am mapping the data later in the program)
const getDogBreeds = async () => {
try {
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds() // -> you are not awaiting this
}, []);
Do this instead
useEffect(() => {
axios.get('https://dog.ceo/api/breeds/list/all')
.then(res => {
const newDogList = generateDogsArray(res.data['message']);
const generatedDogs = selectedDogs(newDogList);
setDogBreeds(generatedDogs);
})
.catch(err => console.log(err));
}, []);
I know this looks awful, but I don't think you should use async/await inside useEffect
Use this in your application
useEffect will update whenever dogBreedsTest is changed. In order to make it work, start with null values and update them to the correct initial values once your async operation is finished.
const { dogBreedsTest } = useApplicationData();
const [dogBreeds, updateDogBreeds] = useState(null);
const [breedList1, updateBreedList1] = useState(null);
useEffect(() => {
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0]);
}, [dogBreedsTest]);
The problem is, that react first render and then run useEffect(), so if you don't want to render nothing before the axios, you need to tell to react, that the first render is null.
Where is your map function, to see the code? to show you it?.
I suppose that your data first is null. So you can use something like.
if(!data) return null
2nd Option:
In your map try this:
{breedList1 === null
? null
: breedList1.map(({id, name, rank}, index) => (
<Draggable
key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable> ))}
You have null, because your axios is async and react try to render before any effect. So if you say to react that the list is null, react will render and load the data from the api in the second time.
Option 1 use the optional chaining operator
dogBreedsTest?.map()
Option 2 check in the return if dogBreedsTest is an array
retrun (<>
{Array.isArray(dogBreedsTest) && dogBreedsTest.map()}
</>)
Option 3 return early
if (!Array.isArray(dogBreedsTest)) return null
retrun (<>
{dogBreedsTest.map()}
</>)
Option 4 set initial state
const [dogBreedsTest, setDogBreeds] = useState([]);
You could also add a loading state and add a loading spinner or something like that:
const [dogBreedsTest, setDogBreeds] = useState(null);
const [loading, setLoading] = useState(true)
const getDogBreeds = async () => {
setLoading(true)
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
setLoading(false)
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
loading,
setDogBreeds
}
Edit
Try to use a useEffect hook to update the states when dogBreedsTest got set.
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest?.[0] ?? []);
const [breedList1, updateBreedList1] = useState(dogBreedsTest?.[0] ?? [])
useEffect(() => {
updateDogBreeds(dogBreedsTest?.[0] ?? [])
updateBreedList1(dogBreedsTest?.[0] ?? [])
}, [dogBreedsTest])

Why are the elements from a mapping function not rendering?

Summarize the problem
I have a page within a Gatsby JS site that accepts state via a provider, and some of that activity is able to be used, however, I am unable to provide the contents from a mapping function that is given via context.
Expected result: the expected elements from the mapping function would render
Actual result: the elements in question are not rendered
No error messages
Describe what you've tried
I thought the issue was not explicitly entering in return on the arrow function in question, but that does not change any of the output
Also, rather than try to access the method directly on the page (via a context provider) I moved the method directly into the Provider hook. This did not change any of the rendering.
Show some code
here is Provider.js
import React, { useState, useEffect } from 'react';
import he from 'he';
export const myContext = React.createContext();
const Provider = props => {
const [state, setState] = useState({
loading: true,
error: false,
data: [],
});
const [page, setPage] = useState(1);
const [score, setScore] = useState(0);
const [correctAnswers, setCorrectAnswers] = useState([]);
const [allQuestions, setAllQuestions] = useState([]);
const [answers, setAnswers] = useState([]);
const [right, setRight] = useState([]);
const [wrong, setWrong] = useState([]);
function clearScore() {
updatedScore = 0;
}
function clearRights() {
while (rights.length > 0) {
rights.pop();
}
}
function clearWrongs() {
while (wrongs.length > 0) {
wrongs.pop();
}
}
let updatedScore = 0;
let rights = [];
let wrongs = [];
const calcScore = (x, y) => {
for (let i = 0; i < 10; i++) {
if (x[i] === y[i]) {
updatedScore = updatedScore + 1;
rights.push(i);
} else wrongs.push(i);
}
}
useEffect(() => {
fetch('https://opentdb.com/api.php?amount=10&difficulty=hard&type=boolean')
.then(response => {
return response.json()
})
.then(json => {
const correctAnswer = json.results.map(q => q['correct_answer']);
const questionBulk = json.results.map(q => q['question']);
setState({
data: json.results,
loading: false,
error: false,
});
setCorrectAnswers(correctAnswers.concat(correctAnswer));
setAllQuestions(allQuestions.concat(questionBulk));
})
.catch(err => {
setState({error: err})
})
}, [])
return (
<myContext.Provider
value={{
state, page, score, answers, right, wrong,
hitTrue: () => {setAnswers(answers.concat('True')); setPage(page + 1);},
hitFalse: () => {setAnswers(answers.concat('False')); setPage(page + 1);},
resetAll: () => {
setAnswers([]);
setPage(1);
setScore(0);
setRight([]);
setWrong([]);
clearScore();
clearWrongs();
clearRights();
},
calculateScore: () => calcScore(answers, correctAnswers),
updateScore: () => setScore(score + updatedScore),
updateRight: () => setRight(right.concat(rights)),
updateWrong: () => setWrong(wrong.concat(wrongs)),
showRightAnswers: () => {right.map((result, index) => {
return (
<p className="text-green-300 text-sm" key={index}>
+ {he.decode(`${allQuestions[result]}`)}
</p>)
})},
showWrongAnswers: () => {wrong.map((result, index) => {
return (
<p className="text-red-500 text-sm" key={index}>
- {he.decode(`${allQuestions[result]}`)}
</p>
)
})},
}}
>
{props.children}
</myContext.Provider>
);
}
export default ({ element }) => (
<Provider>
{element}
</Provider>
);
^the showRightAnswers() and showWrongAnswers() methods are the ones I am trying to figure out
and here is the results.js page.{context.showRightAnswers()} and {context.showWrongAnswers()} are where the mapped content is supposed to appear.
import React from 'react';
import Button from '../components/Button';
import { navigate } from 'gatsby';
import { myContext } from '../hooks/Provider';
const ResultsPage = () => {
return (
<myContext.Consumer>
{context => (
<>
<h1 className="">You Finished!</h1>
<p className="">Your score was {context.score}/10</p>
{context.showRightAnswers()}
{context.showWrongAnswers()}
<Button
buttonText="Try Again?"
buttonActions={() => {
context.resetAll();
navigate('/');
}}
/>
</>
)}
</myContext.Consumer>
);
}
export default ResultsPage;
You are returning inside your map, but you're not returning the map call itself - .map returns an array, and you have to return that array from your "show" functions, e.g.
showWrongAnswers: () => { return wrong.map((result, index) ...
^^^^
This will return the array .map generated from the showWrongAnswers function when it's called, and thus {context.showWrongAnswers()} will render that returned array

Couldn't correctly initialize state in parent component from children states

I have two React components, namely, Form and SimpleCheckbox.
SimpleCheckbox uses some of the Material UI components but I believe they are irrelevant to my question.
In the Form, useEffect calls api.getCategoryNames() which resolves to an array of categories, e.g, ['Information', 'Investigation', 'Transaction', 'Pain'].
My goal is to access checkboxes' states(checked or not) in the parent component(Form). I have taken the approach suggested in this question.(See the verified answer)
Interestingly, when I log the checks it gives(after api call resolves):
{Pain: false}
What I expect is:
{
Information: false,
Investigation: false,
Transaction: false,
Pain: false,
}
Further More, checks state updates correctly when I click into checkboxes. For example, let's say I have checked Information and Investigation boxes, check becomes the following:
{
Pain: false,
Information: true,
Investigation: true,
}
Here is the components:
const Form = () => {
const [checks, setChecks] = useState({});
const [categories, setCategories] = useState([]);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
useEffect(() => {
api
.getCategoryNames()
.then((_categories) => {
setCategories(_categories);
})
.catch((error) => {
console.log(error);
});
}, []);
return (
{categories.map(category => {
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
/>
}
)
}
const SimpleCheckbox = ({ onCheck, label, id }) => {
const [check, setCheck] = useState(false);
const handleChange = (event) => {
setCheck(event.target.checked);
};
useEffect(() => {
onCheck(check, id);
}, [check]);
return (
<FormControl>
<FormControlLabel
control={
<Checkbox checked={check} onChange={handleChange} color="primary" />
}
label={label}
/>
</FormControl>
);
}
What I was missing was using functional updates in setChecks. Hooks API Reference says that: If the new state is computed using the previous state, you can pass a function to setState.
So after changing:
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
to
const handleCheckChange = (isChecked, category) => {
setChecks(prevChecks => { ...prevChecks, [category]: isChecked });
}
It has started to work as I expected.
It looks like you're controlling state twice, at the form level and at the checkbox component level.
I eliminated one of those states and change handlers. In addition, I set checks to have an initialState so that you don't get an uncontrolled to controlled input warning
import React, { useState, useEffect } from "react";
import { FormControl, FormControlLabel, Checkbox } from "#material-ui/core";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form />
</div>
);
}
const Form = () => {
const [checks, setChecks] = useState({
Information: false,
Investigation: false,
Transaction: false,
Pain: false
});
const [categories, setCategories] = useState([]);
console.log("checks", checks);
console.log("categories", categories);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
};
useEffect(() => {
// api
// .getCategoryNames()
// .then(_categories => {
// setCategories(_categories);
// })
// .catch(error => {
// console.log(error);
// });
setCategories(["Information", "Investigation", "Transaction", "Pain"]);
}, []);
return (
<>
{categories.map(category => (
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
check={checks[category]}
/>
))}
</>
);
};
const SimpleCheckbox = ({ onCheck, label, check }) => {
return (
<FormControl>
<FormControlLabel
control={
<Checkbox
checked={check}
onChange={() => onCheck(!check, label)}
color="primary"
/>
}
label={label}
/>
</FormControl>
);
};
If you expect checks to by dynamically served by an api you can write a fetchHandler that awaits the results of the api and updates both slices of state
const fetchChecks = async () => {
let categoriesFromAPI = ["Information", "Investigation", "Transaction", "Pain"] // api result needs await
setCategories(categoriesFromAPI);
let initialChecks = categoriesFromAPI.reduce((acc, cur) => {
acc[cur] = false
return acc
}, {})
setChecks(initialChecks)
}
useEffect(() => {
fetchChecks()
}, []);
I hardcoded the categoriesFromApi variable, make sure you add await in front of your api call statement.
let categoriesFromApi = await axios.get(url)
Lastly, set your initial slice of state to an empty object
const [checks, setChecks] = useState({});

Categories