State update doesn't trigger rerender in conditional rendering component - reactjs

My component roughly looks like this:
import useCustomHook from "./hooks";
const Test = () => {
const [data, setData] = useCustomHook("example-key", {ready: false});
return (
data.ready ?
<>
{console.log("data is ready");}
<ComponentA />
</> :
<>
{console.log("data is not ready");}
<ComponentB />
</>
)
}
useCustomHook is helper hook that pulls from AsyncStorage for my native application, so it has a slight delay. When I run this, I see the console logs "data is not ready" followed by "data is ready", but I only see ComponentB render, and never ComponentA.
If it's helpful, the custom hook looks like this. It basically just serializes the JSON into a string for storage.
export default (key, initialValue) => {
const [storedValue, setStoredValue] = React.useState(initialValue);
React.useEffect(() => {
const populateStoredValue = async () => {
const storedData = await AsyncStorage.getItem(key);
if (storedData !== null) {
setStoredValue(JSON.parse(storedData))
}
}
populateStoredValue()
}, [initialValue, key]);
const setValue = async (value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
await AsyncStorage.setItem(key, JSON.stringify(valueToStore));
setStoredValue(valueToStore);
}
return [storedValue, setValue];
}
Anyone have ideas on what might be happening here?
Thanks!
small PS: I also see the warning Sending "onAnimatedValueUpdate" with no listeners registered., which seems like it's a react-navigation thing that's unrelated. But just wanted to put that here.

First of all, as your key param in custom hook is undefined so data will never be set. Pass the key to the custom hook as the prop.
Secondly, you need to update your condition to check if data is present or not, assuming ready property exist on data after set, like this:
import useCustomHook from "./hooks";
const Test = () => {
const [data, setData] = useCustomHook(/* Add key here */);
return (
data && data.ready ?
<>
console.log("data is ready");
<ComponentA />
</> :
<>
console.log("data is not ready");
<ComponentB />
</>
)
}

Related

Loader that doesn't render children unless data is fetched

I am trying to make a component that renders "children" prop only "and only if" a boolean is true, now i noticed if i do something like this
const QueryLoader = (props) => {
if (!props.isSuccess) return <h2>Loading</h2>;
return props.children;
};
and use it as follows
const Main = (props) => {
const {isSuccess,data} = fetcher("api");
return (
<QueryLoader isSuccess={isSuccess}>
<div>{data.arrayOfData.innerSomething}</div>
</QueryLoader>
);
};
the data.arrayOfData.innerSomething is still triggered which is causing me issues, i thought about instead of sending children i send a component as a function and then call it inside the QueryLoader but i dont know if this has any side-effects.
Any suggestions?
This is called render prop pattern:
const QueryLoader = ({ isSuccess, children }) => {
return isSuccess ? children() : <h2>Loading</h2>;
};
const Main = () => {
const { isSuccess, data } = fetcher("api");
return (
<QueryLoader isSuccess={isSuccess}>
{() => <div>{data.arrayOfData.innerSomething}</div>}
</QueryLoader>
);
};
For data fetching I hightly recommend using react-query library.

Filtering react component state causes infinite render loop

I'm retrieving some data from an API using useEffect, and I want to be able to filter that returned data using a prop being fed into the component from its parent.
I'm trying to filter the state after it is set by useEffect, however it looks like the component is going into an infinite render loop.
What do I need to do to prevent this?
export default function HomeJobList(props: Props): ReactElement {
const [listings, setListings] = React.useState(null);
useEffect(() => {
const func = async () => {
let res = await service.getListings();
setListings(res);
};
func();
}, []);
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
}
return (
<>
<div>do stuff</div>
</>
);
}
I understand that the use of the setListing function is then causing a rerender after the filtering, which then causes another setListing call. But what's the best way to break this loop?
Should I just have another state value that maintains the last searchTerm used to filter and check against that before filtering?
Or is there a better way?
It's an indinite loop because every time you filter, you set it as a state variable, which causes re-rendering and filtering & setting the variable again - thus a loop.
I suggest you do it all in one place (your useEffect is a good place for that, because it only executes once.
useEffect(() => {
(async () => {
const res = await service.getListings();
const filtered = res.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
})();
}, []);
When the state changes that trigger a re-render, that's why you have an infinite loop; what you have to do is to wrap your filtering in a useEffectthat that depends on the searchTerm prop, like this:
import React, { useEffect } from 'react';
export default function HomeJobList() {
const [listings, setListings] = React.useState(null);
useEffect(() => {
const func = async () => {
let res = await service.getListings();
setListings(res);
};
func();
}, []);
useEffect(() => {
if (listings) {
let filtered = listings.filter(x =>
x.positionTitle.includes(props.searchTerm)
);
setListings(filtered);
}
}, [props.searchTerm]);
return (
<>
<div>do stuff</div>
</>
);
}
You need to create a function that's called inside the JSX you're returning.
Actually, you'll need to render the component every time the Props objects changes. That's achieved by calling the function in the JSX code.
Example:
Function:
const filteredListings = () => {
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => {
x.positionTitle.includes(props.searchTerm));
}
return filtered;
}
}
Return Statement:
return (
<ul>
{
filteredListings().map((listing) =>
<li>{listing.title}</li>
);
}
</ul>
);
What you need is a useEffect which does the filtering when props changes.
Replace this
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
}
with
useEffect(()=>{
if (props.searchTerm) {
setListings(prevListing => {
return prevListing.filter((x) => x.positionTitle.includes(props.searchTerm))
});
}
}, [props.searchTerm] )

Ref always return null in Loadable Component

I am using this react library https://github.com/gregberge/loadable-components to load a Component with Ref to access instance values using useImperativeHandle but ref is always null.
Here is my code
import loadable from '#loadable/component';
export default function ParentComponent(props){
const currentPageRef = useRef();
const[currentPage,setCurrentPage]=useState();
const loadPage= (page= 'home') => {
const CurrentPage = loadable(() => import(`./${page}`));
return (
<CurrentPage
ref={currentPageRef}
/>
);
}
useEffect(() => {
console.log(currentPageRef); //This is always logging current(null);
let pageTitle= currentPageRef.current?.getTitle();
let pageSubTitle= currentPageRef.current?.getSubTitle();
console.log(` Page Title=${pageTitle}`); //This is always coming back as null
console.log(`Page SubTitle=${pageSubTitle}`); //This is also always coming back as null
}, [currentPage]);
return (
<div>
<button onClick={() => {
setCurrentPage(loadPage('youtube));
}}>
LoadPage
</button>
</div>
);
}
Where each of the child components contains a useImperativeHandle to expose instance functions but I can't seem to access any of the functions because currentPageRef is always null
Here is an example of one of the child pages that contains the useImperativeHandle implementation
const YouTubePage= React.forwardRef((props,ref)=>{
const [connected, setConnected] = useState(false);
const getTitle = () => {
return connected ? "Your YouTube Channels" : "YouTube";
}
const getSubTitle = () => {
return connected ? "Publishable content is pushed to only connected channels. You may connect or disconnect channel(s) as appropriate" : "Connect a YouTube account to start posting";
}
useImperativeHandle(ref, () => ({ getTitle, getSubTitle }));
return (<div></div>);
});
Any ideas as to why that might be happening?
Thank you
From your code example your aren't actually rendering the component which you set by the state setter:
export default function ParentComponent(props) {
//...
// Render the page
return (
<>
{currentPage}
<div>...</div>
</>
);
}

Loading spinner not showing in React Component

I am creating a React.js app which got 2 components - The main one is a container for the 2nd and is responsible for retrieving the information from a web api and then pass it to the child component which is responsible for displaying the info in a list of items. The displaying component is supposed to present a loading spinner while waiting for the data items from the parent component.
The problem is that when the app is loaded, I first get an empty list of items and then all of a sudden all the info is loaded to the list, without the spinner ever showing. I get a filter first in one of the useEffects and based on that info, I am bringing the items themselves.
The parent is doing something like this:
useEffect(() =>
{
async function getNames()
{
setIsLoading(true);
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
useEffect(() =>
{
async function getItems()
{
setIsLoading(true);
const items= await WebAPI.getItems(selectedName);
setAllItems(items);
setIsLoading(false);
};
getTenants();
},[selectedName]);
.
.
.
return (
<DisplayItems items={allItems} isLoading={isLoading} />
);
And the child components is doing something like this:
let spinner = props.isLoading ? <Spinner className="SpinnerStyle" /> : null; //please assume no issues in Spinner component
let items = props.items;
return (
{spinner}
{items}
)
I'm guessing that the problem is that the setEffect is asynchronous which is why the component is first loaded with isLoading false and then the entire action of setEffect is invoked before actually changing the state? Since I do assume here that I first set the isLoading and then there's a rerender and then we continue to the rest of the code on useEffect. I'm not sure how to do it correctly
The problem was with the asynchronicity when using mulitple useEffect. What I did to solve the issue was adding another spinner for the filters values I mentioned, and then the useEffect responsible for retrieving the values set is loading for that specific spinner, while the other for retrieving the items themselves set the isLoading for the main spinner of the items.
instead of doing it like you are I would slightly tweak it:
remove setIsLoading(true); from below
useEffect(() =>
{
async function getNames()
{
setIsLoading(true); //REMOVE THIS LINE
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
and have isLoading set to true in your initial state. that way, it's always going to show loading until you explicitly tell it not to. i.e. when you have got your data
also change the rendering to this:
let items = props.items;
return isLoading ? (
<Spinner className="SpinnerStyle" />
) : <div> {items} </div>
this is full example with loading :
const fakeApi = (name) =>
new Promise((resolve)=> {
setTimeout(() => {
resolve([{ name: "Mike", id: 1 }, { name: "James", id: 2 }].filter(item=>item.name===name));
}, 3000);
})
const getName =()=> new Promise((resolve)=> {
setTimeout(() => {
resolve("Mike");
}, 3000);
})
const Parent = () => {
const [name, setName] = React.useState();
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(false);
const fetchData =(name) =>{
if(!loading) setLoading(true);
fakeApi(name).then(res=>
setData(res)
)
}
const fetchName = ()=>{
setLoading(true);
getName().then(res=> setName(res))
}
React.useEffect(() => {
fetchName();
}, []);
React.useEffect(() => {
if(name)fetchData(name);
}, [name]);
React.useEffect(() => {
if(data && loading) setLoading(false)
}, [data]);
return (
<div>
{loading
? "Loading..."
: data && data.map((d)=>(<Child key={d.id} {...d} />))}
</div>
);
};
const Child = ({ name,id }) =>(<div>{name} {id}</div>)
ReactDOM.render(<Parent/>,document.getElementById("root"))
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>

React why the state is not updating when calling a function to initialize it?

Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};

Resources