Change child's state in react - javascript

function App(){
const [selectedLang, setSelectedLang] = useState(0);
const [langList, setLangList] = useState([]);
useEffect(()=>{
const list = [];
/* i18n.options.supportedLngs is ['en', 'ko'] */
i18n.options.supportedLngs.map((item, i)=>{
list.push(item);
/* Set selectedLang to i18n's default language.(Default is 'ko')*/
if(item === i18n.language){
setSelectedLang(i);
}
})
setLangList(list);
}, [])
useEffect(()=>{
console.debug("langList :", langList, ",", selectedLang); // <- It print correctly ['en', 'ko'], 1
}, [langList, selectedLang])
return(
<Child defaultIndex={selectedLang} childList={langList}/>
)
}
function Child(props){
const {defaultIndex, childList} = props;
const [selected, setSelected] = useState(0);
useState(()=>{
setSelected(defaultIndex);
},[])
return(
<div>
{childList[selected]} //<- But it print 'en'.
</div>
)
}
The code above is a simplified version of my code.
Currently i18n defaults to 'ko'. I want to display 'ko' in Child by setting selectedLang to 'ko's index in App, and giving the index of 'ko' and the entire language array as props to Child.
However, Child's selected and defaultIndex doesn't seem to change from a state initialized with useState(0).
Can someone help me?

setSelected need to be called after changing defaultIndex in Child component.
And you didn't use proper hook.
useEffect(()=>{
setSelected(defaultIndex);
},[defaultIndex])

You can use react context to achieve this:
First make a Lang provider in app.js
const LangContext = React.createContext('ko');
function App() {
const [lang, setLang] = useState("ko");
return (
<LangContext.Provider value={[lang, setLang]}>
<Toolbar />
</LangContext.Provider>
);
}
Then in any child component you can use :
function Button() {
const [lang, setLang]= useContext(LangContext);
return (
<button onClick={()=>{setLang('en')}}>
{lang}
</button>
);
}
Any change in context will propagate in all components using useContext.
Read more here : https://reactjs.org/docs/hooks-reference.html#usecontext

Related

React sub-component expander and shared state

I'm wanting to create an expandable section with heading that when clicked toggles the expandable section to show/hide.
I have done this before with regular components etc, but this time I am trying to do this with sub-components and am coming a bit stuck with how to get the state working...
Should I be trying to pass the states into the sub-components directly in the main expander component, or should I be trying to use a context to share the state?
For context, I was reading this article which didn't delve into passing functions (helpful, I know).
App.js
const App = () => (
<div>
<Dropdown>
<Dropdown.Title>Dropdown One</Dropdown.Title>
<Dropdown.Body>Some content in the body</Dropdown.Body>
</Dropdown>
</div>
);
useExpandDropdown.js Custom hook
const useExpandDropdown = (initialState = false) => {
const [isExpanded, setIsExpanded] = useState(initialState);
const toggleExpand = () => setIsExpanded((prev) => !prev);
return [isExpanded, toggleExpand];
};
export default useExpandDropdown;
Expander.js
import useExpandDropdown from "../Hooks/useExpandDropdown";
import DropdownBody from "./DropdownBody";
import DropdownTitle from "./DropdownTitle";
const Dropdown = ({ children }) => {
const [isExpanded, toggleExpand] = useExpandDropdown();
return <div>{children}</div>;
};
Dropdown.Title = DropdownTitle;
Dropdown.Body = DropdownBody;
export default Dropdown;
ExpanderTitle.js
const DropdownTitle = ({ children }) => {
// I want to access the toggleExpand function in here
return <div>{children}</div>;
}
export default DropdownTitle;
ExpanderBody.js
const DropdownBody = ({ isExpanded, children }) => {
// I want to access the isExpanded state here
return <div>{children}</div>;
}
export default DropdownBody;
There are several ways to do it, and the right choice depends on the specifics—how your components are structured, what they look like and how you're using them.
But for most cases, I would outsource this kind of logic to a 3rd-party library so you can spend time maintaining your app instead. One choice is Headless UI and they have a component called Disclosure that you can use here.
import { Disclosure } from "#headlessui/react";
const App = () => (
<div>
<Disclosure>
<Disclosure.Button>
Dropdown One
</Disclosure.Button>
<Disclosure.Panel>
Some content in the body
</Disclosure.Panel>
</Disclosure>
</div>
);
As you can see, it's very simple, and depending on what exactly you're doing you might not need the Dropdown components at all.
Note that Disclosure.Button renders a button by default, which, depending on your environment, might come with some default styling you might not want. You should either style it or render something different than a button, e.g.:
<Disclosure.Button as={div}>
or
<Disclosure.Button as={DropdownTitle}>
Just remember to add a11y, since it's an interactive element.
One way is to use cloneElement to add the props (isExpanded or toggleExpand) to the children.
I'm using children[0] and children[1] to 'split' the title and body, this could be improved in a number of ways, like [ title, body ] = children if you're sure there are only 2 elements.
Example, press the title to toggle the body
const { useState } = React;
const useExpandDropdown = (initialState = false) => {
const [isExpanded, setIsExpanded] = useState(initialState);
return [isExpanded, () => setIsExpanded((prev) => !prev)];
};
const Dropdown = ({ children }) => {
const [isExpanded, toggleExpand] = useExpandDropdown();
return (
<div>
{React.cloneElement(children[0], { toggleExpand })}
{React.cloneElement(children[1], { isExpanded })}
</div>
)
};
const DropdownTitle = ({ children, toggleExpand }) => <div onClick={toggleExpand}>{children}</div>;
const DropdownBody = ({ children, isExpanded }) => <div>{'Body is: '}{isExpanded ? 'Visible' : 'Hidden'}</div>;
const Example = () => {
return (
<Dropdown>
<DropdownTitle>Title</DropdownTitle>
<DropdownBody>Some content in the body</DropdownBody>
</Dropdown>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Here is another solution, using the render props pattern. In this approach, the state is managed by your main component, and passed to child components as props at render. This is a commonly used patterns in many libraries, e.g. Formik.
The advantage is complete flexibility—your API is open for extension in the future, as you can define the structure of your components without any restrictions. A disadvantage is that it's a little verbose and can result in prop drilling if you child components have several levels of nesting.
const { useState } = React;
const MyDisclosureTitle = ({
children,
onClick,
}) => {
const style = { all: "unset" };
return (
<button onClick={onClick} style={style} type="button">
{children}
</button>
);
};
const MyDisclosureBody = ({ children }) => {
return <div>{children}</div>;
};
const MyDisclosure = ({ children }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => setIsExpanded((prev) => !prev);
const disclosureBag = {
isExpanded,
toggleExpanded,
};
return children(disclosureBag);
};
MyDisclosure.Title = MyDisclosureTitle;
MyDisclosure.Body = MyDisclosureBody;
const Example = () => {
return (
<MyDisclosure>
{({ isExpanded, toggleExpanded }) => (
<div>
<MyDisclosure.Title onClick={toggleExpanded}>
Dropdown One
</MyDisclosure.Title>
{isExpanded && (
<MyDisclosure.Body>Some content in the body</MyDisclosure.Body>
)}
</div>
)}
</MyDisclosure>
);
};
ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Here is a typescript example: https://codesandbox.io/s/react-disclosure-example-bqrtsk

Why setting the state through setTimeout doesn't render the correct props of a child component?

I have right here a component that should simply render a list of items. Also, the component includes an input that filters the list of items. If there is no items, or if the items are being loaded it should display a message.
import { useState } from "react";
export const List = ({ loading, options }) => {
const _options = options ?? [];
const [renderedOptions, setRenderedOptions] = useState(_options);
const [inputValue, setInputValue] = useState("");
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = _options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};
In App.js, I did a setTimeout, to mock a fetch call. However, there is a problem. Although I'm setting the asyncOptions state to be the new list of items, in my <List /> component the options do not seem to display properly.
import { List } from "./List";
import { useState, useEffect } from "react";
const ITEMS = ["list_1", "list_2", "list_3", "list_4", "list_5"];
export default function App() {
const [asyncOptions, setAsyncOptions] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const timeoutId = setTimeout(() => {
setIsLoading(false);
setAsyncOptions(ITEMS);
}, 2000);
return () => clearTimeout(timeoutId);
}, []);
return <List options={asyncOptions} loading={isLoading} />;
}
What is this happening and what is/are the solution(s)?
Sandbox code here: https://codesandbox.io/s/async-list-j97u32
The first time when list component gets rendered, renderedOptions is initialized with []
const [renderedOptions, setRenderedOptions] = useState(options);
But when the state inside the App component changes, it triggers a re-render and henceforth it triggers re-render of List Component. So since you are passing options as argument to useState u might feel it'll update the state automatically but that's not the case
Note -> useState doesn't take into consideration whatever you are passing as argument except for the first time the component loads
.So the useState will return back the initial State which is [] every time the component re-renders
So if you want to see changes you have to add useEffect inside the List component and trigger a state update every time options changes
Change your code too this,
import { useState } from "react";
export const List = ({ options, loading }) => {
console.log("Listt", options);
const [renderedOptions, setRenderedOptions] = useState([...options]);
const [inputValue, setInputValue] = useState("");
console.log(renderedOptions);
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
useEffect(() => {
setRenderedOptions(options)
} , [options])
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};
Basically, in the beginning, the value of options in an empty array, and the value put in state is a copy of that so the component is not listening to changes on the prop.
For some reason, you have to use the useEffect hook to actively listen to changes in the prop. By using the hook, when the API call returns something, it will set the state.(BTW, if anyone knows what is going on tell us)
I would recommend moving the API call to the List component, it would better encapsulate the logic
import { useEffect, useState } from "react";
export const List = ({ loading, options }) => {
const [renderedOptions, setRenderedOptions] = useState(options);
const [inputValue, setInputValue] = useState("");
useEffect(() => {
setRenderedOptions(options);
}, [options]);
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};

Next.js's per page layout component doesn't get value from Vercel's swr global configuration

The useSWR hook from swr works everywhere if I explicitly enter the fetcher.
const { data } = useSWR("http://...", fetcher);
However, if I used swr global configuration as shown below, the useSWR only works in First page but not in HeaderLayout component. I did some debugging and found out that in HeaderLayout doesn't receive the value from swr global configuration (SWRConfig in _app.tsx) even though it is wrapped inside.
I followed this doc https://nextjs.org/docs/basic-features/layouts#per-page-layouts for the page layout implementation
// _app.tsx
type NextPageWithLayout = NextPage & {
getLayout?: (page: React.ReactElement) => React.ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<SWRConfig
value={{
fetcher: (resource, init) =>
fetch(resource, init).then((res) => res.json()),
}}
>
{getLayout(<Component {...pageProps} />)}
</SWRConfig>
);
}
// pages/first
const First = () => {
const [searchInput, setSearchInput] = useState("");
const router = useRouter();
const { data } = useSWR("http://...");
return (
<div>...Content...</div>
);
};
First.getLayout = HeaderLayout;
// layout/HeaderLayout
const HeaderLayout = (page: React.ReactElement) => {
const router = useRouter();
const { project: projectId, application: applicationId } = router.query;
const { data } = useSWR(`http://...`);
return (
<>
<Header />
{page}
</>
);
};
Helpful links:
https://nextjs.org/docs/basic-features/layouts#per-page-layouts
https://swr.vercel.app/docs/global-configuration
Next.js context provider wrapping App component with page specific layout component giving undefined data
Your First.getLayout property should be a function that accepts a page and returns that page wrapped by the HeaderLayout component.
First.getLayout = function getLayout(page) {
return (
<HeaderLayout>{page}</HeaderLayout>
)
}
The HeaderLayout is a React component, its first argument contains the props passed to it. You need to modify its signature slightly to match this.
const HeaderLayout = ({ children }) => {
const router = useRouter();
const { project: projectId, application: applicationId } = router.query;
const { data } = useSWR(`http://...`);
return (
<>
<Header />
{children}
</>
);
};
Layouts doesnt work if you declare Page as const. So instead of const First = () => {...} do function First() {...}

Reactjs not updating the State in functional component

I am developing a simple application in reactS. The main purpose of the app is it will show some cards and on search, cards will be filtered and selective cards will be displayed. I am sharing the code of App.js.
I have a file name 'Robots.js'
import './App.css';
import CardList from './Components/CardList';
import Header from './Components/Header';
import {Robots} from './Components/Robots';
function App() {
const [robots,setRobots] = useState({
robo:Robots,
search:''
});
const onSearchChange = (e) =>{
setRobots({...robots,search:e.target.value});
const filteredRobots = robots.robo.filter(item=>{return item.name.toLowerCase().includes(robots.search.toLowerCase())});
//setRobots({...robots,robo:filteredRobots});
console.log(filteredRobots);
}
return (
<div>
<Header onSearchChange={onSearchChange} />
<CardList Robots={robots.robo}/>
</div>
);
}
export default App;
If I comment
setRobots({...robots,robo:filteredRobots});
this line on console I can show array is reducing its number of items but with that line it just does nothing. I think it makes sense it should do.
I hope I made my point clear.
You can update the state in one go as shown below:
const onSearchChange = (e) => {
const filteredRobots = robots.robo.filter(item => {
return item.name.toLowerCase().includes(robots.search.toLowerCase())
});
console.log(filteredRobots);
setRobots({
robo: filteredRobots,
search: e.target.value
});
}
Since you only have two properties you can just create a new object with those properties and don't really need spread operator.
The setRobots run "asynchronous" - therefore if you have:
const[data, setData] = setState(5);
const update = (newData) => {
setData(1);
console.log(data) // will print 5
}
And only in the second render its will print 1
you can read more in here: https://reactjs.org/docs/state-and-lifecycle.html
In your implementation you have a problem that you don't save the original array of the cards
So first save the original array
// Here will be the full list of cards.
const [cards, setCards] = useState(allCards);
// Here will be the search by text
const [search, setSearch] = useState("");
// Here will be all the filtered cards
// If you don't know whats useMemo is you can look in here: https://reactjs.org/docs/hooks-reference.html#usememo
const filteredCards = useMemo(()=>{
return cards.filter((card) =>
card.item.toLowerCase().contains(search.toLowerCase()));
},[cards, search])
// on search changed callback
const onSearchChange = (e)=>{setSearch(e.target.value || '')}
// then return your component
return (
<div>
<Header onSearchChange={onSearchChange} />
<CardList Robots={filteredCards}/>
</div>
);

Prevent re-rendering in ReactJS of all childs on onChange event

I am setupping a simple dashboard to challeging my self with ReactJS, but I have some issues preventing useless re-rendering.
I have a root component called App where I fetch some data.
const App = () => {
const [data, setData] = useState(null);
const [list1, setList1] = useState(null);
const [list2, setList2] = useState(null);
const [list3, setList3] = useState(null);
const [list4, setList4] = useState(null);
useEffect(() => {
const fetchData = fetchDataInSomeWay();
const fetchedData = getData(fetchData);
const list1Data = getList1(fetchData);
setList1(list1Data);
setData(fetchedData);
});
...
{ data !== null
&& (
<Parent
data={data}
list1={list1}
list2={list2}
list3={list3}
list4={list4}
/>
};
Then I setup a Parent component where I created some Select component and other elements which depend on the values ​​selected by select.
I have a Select element for each list state created with useState();
const Google = ({
data,
list1,
list2,
list3,
list4,
}) => {
const [typeValue, setTypeValue] = useState('someValue');
const [list1Value, setList1Value] = useState(list1[0]);
const [list2Value, setList2Value] = useState(list2[0]);
const [list3Value, setList3Value] = useState(list3[0]);
const [list4Value, setList4Value] = useState(list4[0]);
const onChangeSelectTypeValue = (value) => {
setTypeValue(value);
};
...
const selectTypeValueElement = (
<SelectElement
select={selectType}
value={[typeValue]}
onChangeValue={onChangeSelectTypeValue}
values={list1Value}
/>
);
...
<div className="interactionHeaderChart">
{ selectTypeValueElement }
...
</div>
};
Then I have a Select element where I do not store a state, but where option selected is passed to Parent compoment.
const SelectElement = ({
select, value, values, onChangeValue,
}) => {
...
<Select
...
value={value[0]}
onChange={onChangeValue}
>
...
};
Now when I select some option from one Select, state of Parent change and all Childs re-render, all Selects components and also other components which depend on the values ​​selected by select.
Can I prevent all Select components from re-rendering? Can I avoid to re-render all other components which does not depend on the values of option selected?
The fact that the state has changed from the onChange function and not from useEffect() is confusing me and I can not understand how to solve it.
Thanks.
You should look into shouldComponentUpdate:
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
Usually, in order to use this with your SelectElement component you will first have to convert it into a Class. You can then add the shouldComponentUpdate function to it and check the previous and next props are the same or not. If they are the same, don't update.
However, if your props are not complex objects, you can actually just recreate your SelectElement as a PureComponent. This will automatically check the props and will not re-render if they're the same.
e.g.
class SelectElement extends React.PureComponet {...
you can use memo to avoid re rendering.
Way 1:
const NestedComponent = () => {
return (
<div>
ContainerComponent
</div>
);
};
export default React.memo(NestedComponent);
Way 2:
function ParentComponent(a, b) {
const childComponent = React.useMemo(() => <ChildComponent posts={a} />, [a]);
return (
<>
{childComponent}
</>
)
}

Categories