Hello I am using a switch statement to serve particular components to a page In my next js project. The switch statement receives a payload which it loops through in order to derive what component to serve. These components have been imported dynamically and I now wish to use this dynamic importing along with the Intersection Observer to load components when they come in the viewport to decrease the Initial page load time and split up the chunks. I have incorporated a hook that uses the intersection observer along with use ref to try to replicate my idea. Now this works when I give the reference to one div and it observes the component coming into the viewport as expected, however when I add multiple refs to my divs, I still only get the one div being observed with the ref.
What am I doing wrong? I thought you could reference the same ref multiple times and just use .current to identify the current element being observed?
Switch Statement:
import React from 'react';
import getTCTEnv from '../../../lib/helpers/get-tct-env';
import IconWishlistButton from '../../wishlist/add-to-wishlist-button/button-types/icon-wishlist-button';
import loadable from '#loadable/component';
import { useOnScreen } from '../../../hooks/on-screen';
const PriorityCollection = loadable(
() => import('#culture-trip/tile-ui-module/dist/collectionRail/PriorityCollections'),
{
resolveComponent: (components) => components.PriorityCollection
}
);
const TravelWithUs = loadable(
() => import('../../../components/trips/travel-with-us/travel-with-us'),
{
resolveComponent: (components) => components.TravelWithUs
}
);
const TrustMessaging = loadable(() => import('../../../components/trips/trust-messaging/index'), {
resolveComponent: (components) => components.TrustMessaging
});
const PressMessaging = loadable(() => import('../../../components/trips/press-messaging'), {
resolveComponent: (components) => components.PressMessaging
});
const TripsChatBanner = loadable(
() => import('../../../components/trips/chat-banner/chat-banner'),
{
resolveComponent: (components) => components.TripsChatBanner
}
);
const HpFeaturedArticles = loadable(
() => import('../home-page-featured-articles/home-page-featured-articles'),
{
resolveComponent: (components) => components.HpFeaturedArticles
}
);
const InstagramSection = loadable(() => import('../../../components/trips/instagram'), {
resolveComponent: (components) => components.InstagramSection
});
const EmailForm = loadable(() => import('../../../components/trips/email-form'));
const ReviewsSection = loadable(() => import('../../../components/trips/reviews'));
export const IncludeComponent = ({ collections, reviewData, type }) => {
const [containerRef, isVisible] = useOnScreen({
root: null,
rootMargin: '0px',
threshold: 0.1
});
const instagramCollection = collections.filter((collection) => collection.type === 'instagram');
const getComponents = () =>
collections.map((el, i) => {
switch (el.type) {
case 'trips':
case 'article':
return (
<PriorityCollection
key={i}
collections={[el]}
tctEnv={getTCTEnv()}
wishlistButton={<IconWishlistButton />}
/>
);
case 'reviews':
return (
<>
<div ref={containerRef} id={i}></div>
<ReviewsSection reviewData={reviewData} />
</>
);
case 'instagram':
return (
<>
<div ref={containerRef} id={i}></div>
<InstagramSection collection={instagramCollection} />
</>
);
case 'featured':
return <PressMessaging />;
case 'trust':
return <TrustMessaging type={type} />;
case 'featuredArticle':
return <HpFeaturedArticles />;
case 'email':
return <EmailForm />;
case 'chat':
return <TripsChatBanner />;
case 'travel':
return <TravelWithUs type={type} />;
default:
return;
}
});
return getComponents();
};
custom hook:
import { useEffect, useState, useRef } from 'react';
export const useOnScreen = (options): any => {
const containerRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState([]);
const callbackFunction = (entries) => {
const [entry] = entries;
if (entry.isIntersecting)
setIsVisible((oldArray) => [
...oldArray,
isVisible.indexOf(entry.target.id) === -1 && entry.target.id !== undefined
? entry.target.id
: console.log('nothing')
]);
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef.current);
return () => {
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, [containerRef.current, options]);
return [containerRef, isVisible];
};
Currently only the instagram ref gets observed
If I understand your code correctly, more than one component is possibly rendered from getComponents.
For instance, the tree could contain:
<div ref={containerRef} id={i}></div>
<ReviewsSection reviewData={reviewData} />
<div ref={containerRef} id={i}></div>
<InstagramSection collection={instagramCollection} />
And you want both divs there to be observed.
It doesn't work because the ref doesn't trigger the effect by itself. The ref is simply an object like { current: null }.
When the tree is rendered, containerRef.current is set to the first div, then it is set to the second div, then the effect runs.
To do what you want you can:
Call the custom hook multiple times, and assign one containerRef to each div. The issue here is, of course, you will also have multiple IntersectionObservers instances.
Declare multiple refs and pass them to the custom hook via argument, instead of returning the ref from the custom hook.
Implement a callback ref that adds every div to a list, skipping duplicates. This one allows you to keep the same implementation in getComponents, but is also the trickiest for the hook.
Solved with this:
import React, { useEffect, useReducer } from 'react';
import getTCTEnv from '../../../lib/helpers/get-tct-env';
import IconWishlistButton from '../../wishlist/add-to-wishlist-button/button-types/icon-wishlist-button';
import loadable from '#loadable/component';
import { useOnScreen } from '../../../hooks/on-screen';
const PriorityCollection = loadable(
() => import('#culture-trip/tile-ui-module/dist/collectionRail/PriorityCollections'),
{
resolveComponent: (components) => components.PriorityCollection
}
);
const TravelWithUs = loadable(
() => import('../../../components/trips/travel-with-us/travel-with-us'),
{
resolveComponent: (components) => components.TravelWithUs
}
);
const TrustMessaging = loadable(() => import('../../../components/trips/trust-messaging/index'), {
resolveComponent: (components) => components.TrustMessaging
});
const PressMessaging = loadable(() => import('../../../components/trips/press-messaging'), {
resolveComponent: (components) => components.PressMessaging
});
const TripsChatBanner = loadable(
() => import('../../../components/trips/chat-banner/chat-banner'),
{
resolveComponent: (components) => components.TripsChatBanner
}
);
const HpFeaturedArticles = loadable(
() => import('../home-page-featured-articles/home-page-featured-articles'),
{
resolveComponent: (components) => components.HpFeaturedArticles
}
);
const InstagramSection = loadable(() => import('../../../components/trips/instagram'), {
resolveComponent: (components) => components.InstagramSection
});
const EmailForm = loadable(() => import('../../../components/trips/email-form'));
const ReviewsSection = loadable(() => import('../../../components/trips/reviews'));
export const IncludeComponent = ({ collections, reviewData, type }) => {
const [containerRef, isVisible] = useOnScreen({
root: null,
rootMargin: '0px',
threshold: 0.1
});
const instagramCollection = collections.filter((collection) => collection.type === 'instagram');
const getComponents = () =>
collections.map((el, i) => {
switch (el.type) {
case 'trips':
case 'article':
return (
<PriorityCollection
key={i}
collections={[el]}
tctEnv={getTCTEnv()}
wishlistButton={<IconWishlistButton />}
/>
);
case 'reviews':
return (
<>
<div
ref={(element) => {
containerRef.current[i] = element;
}}
id={i}
></div>
{isVisible.indexOf(i.toString()) !== -1 && <ReviewsSection reviewData={reviewData} />}
</>
);
case 'instagram':
return (
<>
<div
ref={(element) => {
containerRef.current[i] = element;
}}
id={i}
></div>
<InstagramSection collection={instagramCollection} />
</>
);
case 'featured':
return <PressMessaging />;
case 'trust':
return <TrustMessaging type={type} />;
case 'featuredArticle':
return <HpFeaturedArticles />;
case 'email':
return <EmailForm />;
case 'chat':
return <TripsChatBanner />;
case 'travel':
return <TravelWithUs type={type} />;
default:
return;
}
});
return getComponents();
};
hook:
import { useEffect, useState, useRef } from 'react';
export const useOnScreen = (options): any => {
const containerRef = useRef<HTMLDivElement[]>([]);
const [isVisible, setIsVisible] = useState([]);
const callbackFunction = (entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
const checkIdInArray = isVisible.indexOf(entry.target.id) === -1;
if (checkIdInArray) setIsVisible((oldArray) => [...oldArray, entry.target.id]);
}
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current)
containerRef.current.forEach((el) => {
observer.observe(el);
});
return () => {
if (containerRef.current)
containerRef.current.forEach((el) => {
observer.unobserve(el);
});
};
}, [containerRef, options]);
return [containerRef, isVisible];
};
Related
I have a Modelmenu that is nested in the parent component, it implements the function of opening a modal window by click. Also in this parent component there is a child component to which you need to bind the function of opening a modal window by one more element. I can do this if all the logic is in the parent component, but this is a bad practice, since I will have to duplicate the code on each page in this way. How can I do this? I'm sorry, I'm quite new, I can't understand the callback.
Parent:
const Home: NextPage = () => {
const handleCallback = (handleOpen: any) => {
}
return (
<>
<ModalMenu parentCallback={handleCallback}/>
<Slider handleCallback={handleCallback}/>
</>
)
}
Modal:
export const ModalMenu: FC = (props) => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
props.parentCallback(setPlay(!play))
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return
}
Child:
export const Slider: FC= (props) => {
return (
<>
<Image nClick={props.handleCallback}/>
</>
I did as advised in the comments using hook, it works fine, maybe it will be useful to someone. Custom hook is really convenient
export const useModal = () => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
setPlay(!play)
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return {
play, handleOpen, handleClose
}
}
If you're looking to pass down values and/or functions from a parent to two or more children, I think it's better to just have the values and functions in the parent
Parent :
const Home: NextPage = () => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
setPlay(prev => !prev)
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return (
<>
<ModalMenu handleOpen={handleOpen} handleClose={handleClose} play={play}/>
<MainSlider <ModalMenu handleOpen={handleOpen} handleClose={handleClose} play={play}/>
</>
)
}
if you want to pass an interface to the props in the children for typescript your interface will look something like this
interface iProps {
play : boolean;
handleOpen : () => void;
handleClose : () => void;
}
export const ModalMenu: FC = (props):iProps => {
// you can easily access all you want
const {handleClose, handleOpen, play} = props
return
}
export const Slider: FC= (props): iProps => {
const {handleClose, handleOpen, play} = props
return (
<>
<Image onClick={handleOpen}/>
</>
I'm refactoring some old code for an alert widget and am abstracting it into its own component that uses DOM portals and conditional rendering. I want to keep as much of the work inside of this component as I possibly can, so ideally I'd love to be able to expose the Alert component itself as well as a function defined inside of that component triggers the render state and style animations so that no outside state management is required. Something like this is what I'm looking to do:
import Alert, { renderAlert } from '../Alert'
const CopyButton = () => (
<>
<Alert text="Text copied!" />
<button onClick={() => renderAlert()}>Copy Your Text</button>
</>
)
Here's what I currently have for the Alert component - right now it takes in a state variable from outside that just flips when the button is clicked and triggers the useEffect inside of the Alert to trigger the renderAlert function. I'd love to just expose renderAlert directly from the component so I can call it without the additional state variable like above.
const Alert = ({ label, color, stateTrigger }) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger])
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
};
export default Alert;
Though it's not common to "reach" into other components and invoke functions, React does allow a "backdoor" to do so.
useImperativeHandle
React.forwardRef
The idea is to expose out the renderAlert function imperatively via the React ref system.
Example:
import { forwardRef, useImperativeHandle } from 'react';
const Alert = forwardRef(({ label, color, stateTrigger }, ref) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger]);
useImperativeHandle(ref, () => ({
renderAlert,
}));
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
});
export default Alert;
...
import { useRef } from 'react';
import Alert from '../Alert'
const CopyButton = () => {
const ref = useRef();
const clickHandler = () => {
ref.current?.renderAlert();
};
return (
<>
<Alert ref={ref} text="Text copied!" />
<button onClick={clickHandler}>Copy Your Text</button>
</>
)
};
A more React-way to accomplish this might be to abstract the Alert state into an AlertProvider that renders the portal and handles the rendering of the alert and provides the renderAlert function via the context.
Example:
import { createContext, useContext, useState } from "react";
interface I_Alert {
renderAlert: (text: string) => void;
}
const AlertContext = createContext<I_Alert>({
renderAlert: () => {}
});
const useAlert = () => useContext(AlertContext);
const AlertProvider = ({ children }: { children: React.ReactElement }) => {
const [text, setText] = useState<string>("");
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
...
const renderAlert = (text: string): void => {
setAlertRendered(false);
setAlertVisible(false);
setText(text);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
const ele = <div>{alertRendered && <div> ..... </div>}</div>;
return (
<AlertContext.Provider value={{ renderAlert }}>
{children}
// ... portal ...
</AlertContext.Provider>
);
};
...
const CopyButton = () => {
const { renderAlert } = useAlert();
const clickHandler = () => {
renderAlert("Text copied!");
};
return (
<>
<button onClick={clickHandler}>Copy Your Text</button>
</>
);
};
...
function App() {
return (
<AlertProvider>
...
<div className="App">
...
<CopyButton />
...
</div>
...
</AlertProvider>
);
}
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
I need to make a click on the button in one component and on this click call a function in the adjacent one. What's the easiest way?
I implemented like this. https://stackblitz.com/edit/react-l5beyi But I think you can do it much easier. React is new to me, and this construction looks strange ...
const App = () => {
const [isAdded, setIsAdded] = useState(false);
function handleClick(status) {
setIsAdded(status)
}
return (
<div>
<ComponentFirst
HandleClick={handleClick}
/>
<ComponentSecond
isAdded={isAdded}
handleCreate={handleClick}
/>
</div>
);
}
const ComponentFirst = ({ HandleClick }) => {
return (
<button
onClick={HandleClick}
>button</button>
)
}
const ComponentSecond = (props) => {
let { isAdded, handleCreate } = props;
const result = () => {
alert('work')
console.log('work')
}
React.useEffect(() => {
if (isAdded) {
result()
handleCreate(false);
}
}, [isAdded, handleCreate]);
return (
<></>
)
}
In your (contrived, I suppose) example the second component doesn't render anything, so it doesn't exist. The work should be done by the parent component:
const App = () => {
const handleClick = React.useCallback((status) => {
alert(`work ${status}`);
// or maybe trigger some asynchronous work?
}, []);
return (
<div>
<ComponentFirst handleClick={handleClick} />
</div>
);
};
const ComponentFirst = ({ handleClick }) => {
return <button onClick={() => handleClick("First!")}>button</button>;
};
You can also use a CustomEvent to which any component can listen to.
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
return (
<div>
<ComponentFirst />
<ComponentSecond />
</div>
);
}
const ComponentFirst = () => {
const handleClick = (e) => {
// Create the event.
const event = new CustomEvent("myCustomEventName", {
detail: "Some information"
});
// target can be any Element or other EventTarget.
window.dispatchEvent(event);
};
return <button onClick={handleClick}>button</button>;
};
const ComponentSecond = (props) => {
function eventHandler(e) {
console.log("Dispatched Detail: " + e.detail);
}
//Listen for this event on the window object
useEffect(() => {
window.addEventListener("myCustomEventName", eventHandler);
return () => {
window.removeEventListener("myCustomEventName", eventHandler);
};
});
return <></>;
};
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({});