React Native - How to memoize a component correctly? - javascript

This is my parent component:
const A = () => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const isFetching = useRef(false);
...
const fetchMoreData = async () => {
if(isFetching.current) return;
isFetching.current = true;
setIsLoading(true);
try {
const newData = await ...
setData([...data, ...newData]);
}
catch(err) {
...
}
setIsLoading(false);
isFetching.current = false;
}
...
return <B data={data} isLoading={isLoading} onEndReached={fetchMoreData} />
}
And I am trying to memoize my child component B, to avoid unnecessary re-renders. Currently, I am doing the following:
const B = memo(({ data, isLoading, onEndReached }) => {
...
return (
<FlatList
data={data}
isLoading={isLoading}
onEndReached={onEndReached}
/>
);
},
(prevProps, nextProps) => {
return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) &&
prevProps.isLoading === nextProps.isLoading;
});
But, I think, that my memoization can cause problems... as I am not adding prevProps.onEndReached === nextProps.onEndReached
But... all works fine :/ Btw I suppose that there can be a little chance to see unexpected things happening because of not adding it. What do you think? Is it necessary to add methods in the areEqual method?

Related

Empty Object on React useEffect

In my project I have the component ExportSearchResultCSV. Inside this component the nested component CSVLink exports a CSV File.
const ExportSearchResultCSV = ({ ...props }) => {
const { results, filters, parseResults, justify = 'justify-end', fileName = "schede_sicurezza" } = props;
const [newResults, setNewResults] = useState();
const [newFilters, setNewFilters] = useState();
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const [headers, setHeaders] = useState([])
const prepareResults = () => {
let newResults = [];
if (results.length > 1) {
results.map(item => {
newResults.push(parseResults(item));
}); return newResults;
}
}
const createData = () => {
let final = [];
newResults && newResults?.map((result, index) => {
let _item = {};
newFilters.forEach(filter => {
_item[filter.filter] = result[filter.filter];
});
final.push(_item);
});
return final;
}
console.log(createData())
const createHeaders = () => {
let headers = [];
newFilters && newFilters.forEach(item => {
headers.push({ label: item.header, key: item.filter })
});
return headers;
}
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters])
return (
<div className={`flex ${justify} h-10`} title={"Esporta come CSV"}>
{results.length > 0 &&
<CSVLink data={createData()}
headers={headers}
filename={fileName}
separator={";"}
onClick={async () => {
await setNewFilters(filters);
await setNewResults(prepareResults());
await setData(createData());
await setHeaders(createHeaders());
}}>
<RoundButton icon={<FaFileCsv size={23} />} onClick={() => { }} />
</CSVLink>}
</div >
)
}
export default ExportSearchResultCSV;
The problem I am facing is the CSV file which is empty. When I log createData() function the result is initially and empty object and then it gets filled with the data. The CSV is properly exported when I edit this component and the page is refreshed. I tried passing createData() instead of data to the onClick event but it didn't fix the problem. Why is createData() returning an empty object first? What am I missing?
You call console.log(createData()) in your functional component upon the very first render. And I assume, upon the very first render, newFilters is not containing anything yet, because you initialize it like so const [newFilters, setNewFilters] = useState();.
That is why your first result of createData() is an empty object(?). When you execute the onClick(), you also call await setNewFilters(filters); which fills newFilters and createData() can work with something.
You might be missunderstanding useEffect(). Passing something to React.useEffect() like you do
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters]) <-- look here
means that useEffect() is only called, when results or filters change. Thus, it gets no executed upon initial render.

How to handle state in useEffect from a prop passed from infinite scroll component

I have a React component using an infinite scroll to fetch information from an api using a pageToken.
When the user hits the bottom of the page, it should fetch the next bit of information. I thought myself clever for passing the pageToken to a useEffect hook, then updating it in the hook, but this is causing all of the api calls to run up front, thus defeating the use of the infinite scroll.
I think this might be related to React's derived state, but I am at a loss about how to solve this.
here is my component that renders the dogs:
export const Drawer = ({
onClose,
}: DrawerProps) => {
const [currentPageToken, setCurrentPageToken] = useState<
string | undefined | null
>(null);
const {
error,
isLoading,
data: allDogs,
nextPageToken,
} = useDogsList({
pageToken: currentPageToken,
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting) {
setCurrentPageToken(nextPageToken);
}
},
[nextPageToken],
);
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<Drawer
onClose={onClose}
>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
useDogsList essentially looks like this with all the cruft taken out:
import { useEffect, useRef, useState } from 'react';
export const useDogsList = ({
pageToken
}: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
null,
);
const [allDogs, setAllDogs] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result =
await myClient.listDogs(
getDogsRequest,
{
token,
},
);
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken);
// if API returns a pageToken, that means there are more dogs to add to the list
if (nextPageToken) {
setAllDogs((previousDogList) => [
...(previousDogList ?? []),
...newDogs,
]);
}
}
} catch (responseError: unknown) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
};
fetchData();
}, [ pageToken, nextPageToken]);
return {
data: allDogs,
nextPageToken,
error,
isLoading,
};
};
Basically, the api call returns the nextPageToken, which I want to use for the next call when the user hits the intersecting point, but because nextPageToken is in the dependency array for the hook, the hook just keeps running. It retrieves all of the data until it compiles the whole list, without the user scrolling.
I'm wondering if I should be using useCallback or look more into derivedStateFromProps but I can't figure out how to make this a "controlled" component. Does anyone have any guidance here?
I suggest a small refactor of the useDogsList hook to instead return a hasNext flag and fetchNext callback.
export const useDogsList = ({ pageToken }: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
pageToken // <-- initial token value for request
);
const [allDogs, setAllDogs] = useState([]);
// memoize fetchData callback for stable reference
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await myClient.listDogs(getDogsRequest, { token: nextPageToken });
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken ?? null);
setAllDogs((previousDogList) => [...previousDogList, ...newDogs]);
} catch (responseError) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
}, [nextPageToken]);
useEffect(() => {
fetchData();
}, []); // call once on component mount
return {
data: allDogs,
hasNext: !!nextPageToken, // true if there is a next token
error,
isLoading,
fetchNext: fetchData, // callback to fetch next "page" of data
};
};
Usage:
export const Drawer = ({ onClose }: DrawerProps) => {
const { error, isLoading, data: allDogs, hasNext, fetchNext } = useDogsList({
pageToken // <-- pass initial page token
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting && hasNext) {
fetchNext(); // <-- Only fetch next if there is more to fetch
}
},
[hasNext, fetchNext]
);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
// From #stonerose036
// clear previous observer in returned useEffect cleanup function
return observer.disconnect;
}, [handleObserver]);
return (
<Drawer onClose={onClose}>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
Disclaimer
Code hasn't been tested, but IMHO it should be pretty close to what you are after. There may be some minor tweaks necessary to satisfy any TSLinting issues, and getting the correct initial page token to the hook.
While Drew and #debuchet's answers helped me improve the code, the problem around multiple renders ended up being solved by tackling the observer itself. I had to disconnect it afterwards
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
return () => {
observer.disconnect();
};
}, [handleObserver]);

There's a bug with Search and Pagination in React

I'm building my site in React and I have created pagination and search. When I search for something on the site, it only works when after that I go to another page. I think this is due to the fact that Softwares and Pagination are in the same component.
Then I tried lifting-state-up, but I got an error: React Minified Error # 31.
Here's Pagination component:
const Paginator = ({
total, // Total records
startPage = 1,
totalPages = null,
onMovePage = null,
}) => {
...
return (
<>
<section id={styles.paginator}>
<Header/>
...
{range(1, totalPages+1).map(p => (
<PagItem key={p} handleClick={ () => {setCurrentPage(p); onMovePage && onMovePage({currentPage: p})} } title={p} name={p} />
))}
...
</section>
</>
);
};
Here's Softwares component:
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const fetchData = async ({ currentPage }) => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
useEffect(() => {
fetchData({ currentPage: 1 });
}, []);
return (
<>
{
valid &&
<section className={styles.softwares}>
<Header header={"new softwares"} />
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
<Paginator totalPages={totalPages} total={total} onMovePage={fetchData} />
</section>
}
</>
);
};
SearchForm in Header component:
const Header = ({ handleChange, handleClick }) => {
return (
...
<SearchForm handleChange={handleChange} handleClick={handleClick} />
...
);
};
const SearchForm = ({ style, handleChange, handleClick }) => {
return (
<div style={style}>
<form>
<input
type="text"
onChange={handleChange}
/>
<SearchButton onClick={handleClick} />
<small>ENTER</small>
</form>
</div>
);
};
const SearchButton = ({onClick }) => {
return (
<button type="button" onClick={onClick}>
<FontAwesomeIcon icon={faSearch} />
</button>
);
};
And part of Search in App component:
const App = () => {
...
// Search
const [search, setSearch] = useState('');
const [shouldFetch, setShouldFetch] = useState(false);
const handleChange = (e) => {
setSearch(e.target.value);
}
useEffect(() => {
if (shouldFetch) {
(async () => {
const response = await fetch(`http://127.0.0.1:8000/api/software/?search=${search}`);
const data = await response.json();
setShouldFetch(false);
})()
}
}, [shouldFetch]);
const handleClick = () => setShouldFetch(true);
return (
<div className="App">
<Header handleChange={handleChange} handleClick={handleClick} />
...
<Switch>
<Route path="/" exact render={props => <Softwares {...props} search={search} />} />
</Switch>
{/* Actually I'd like to use Paginator here, but it
throws the error: React Minified Error # 31 */}
...
</div>
);
}
So, how can this be done?
The problem is your useEffect dependencies (or lack thereof).
Here's the relevant section of the code:
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const fetchData = async ({ currentPage }) => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
useEffect(() => {
fetchData({ currentPage: 1 });
}, []);
The empty dependency array means that you are running the effect that calls fetchData one time when the component mounts. Clicks in the Pagination component will call the fetchData function directly. Changes to search do not cause fetchData to re-run. The data depends on the search so search should be a dependency.
The fetchData function is fine in this component. The state that I would recommend lifting up is to lift the currentPage up from Pagination into Softwares. The onMovePage callback can just update the currentPage state. That way you can call fetchData only through your effect and run the effect whenever either search or currentPage changes.
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
// defining the function inside of the useEffect
// lets eslint exhaustive dependency checks work their magic
const fetchData = async () => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
// need to define and call in separate steps when using async functions
fetchData();
}, [currentPage, search]);
return (
...
<Paginator page={currentPage} totalPages={totalPages} total={total} onMovePage={setCurrentPage} />
...
);
};

React Native: Saving array via AsyncStorage and retrieving it

I am trying to get an array of objects from my Redux-Store state called user and save it to async storage and use useState with the response to set the state before I retrieve it and view it with the FlatList however I am getting an error along the lines of Warning: Can't perform a React state update on an unmounted component. The user details is being set to the redux store in another component and then being retrieved from the current component I am displaying. Please could I get your help . I would really appreciate it. Thank you in advance!!!
const TheUser = (props) => {
//user is an array from redux store
const user = useSelector(state => state.account.cookbook)
const [getUser, setGetUser] = useState()
const saveUserAsync = async () => {
await AsyncStorage.setItem('user', JSON.stringify(user))
}
saveUserAsync()
AsyncStorage.getItem('user').then(response => {
setGetUser(response)
})
return (
<FlatList
data={getUser}
keyExtractor={item => item.id}
renderItem={itemData =>
<MyUser
name={itemData.item.name}
image={itemData.item.imageUri}
details={itemData.item.details.val}
/>
}
/>
)
}
export default TheUser
You can use useEffect hook to solve this problem.
IS_MOUNTED variable will track if component is mounted or not.
let IS_MOUNTED = false; // global value
const TheUser = (props) => {
//user is an array from redux store
const user = useSelector(state => state.account.cookbook)
const [getUser, setGetUser] = useState()
const saveUserAsync = async () => {
await AsyncStorage.setItem('user', JSON.stringify(user))
}
AsyncStorage.getItem('user').then(response => {
if(IS_MOUNTED)
{
setGetUser(JSON.parse(response));
}
});
useEffect(() => {
IS_MOUNTED = true;
saveUserAsync();
return (() => {
IS_MOUNTED = false;
})
},[])
return (
<FlatList
data={getUser}
keyExtractor={item => item.id}
renderItem={itemData =>
<MyUser
name={itemData.item.name}
image={itemData.item.imageUri}
details={itemData.item.details.val}
/>
}
/>
)
}
export default TheUser
import { useEffect } from "react"
let isMount = true
const TheUser = (props) => {
//user is an array from redux store
const user = useSelector(state => state.account.cookbook)
// const [getUser, setGetUser] = useState()
// useEffect(() => {
// const saveUserAsync = async () => {
// await AsyncStorage.setItem('user', JSON.stringify(user))
// const response = await AsyncStorage.getItem('user')
// if (isMount)
// setGetUser(JSON.parse(response))
// }
// saveUserAsync()
// }, [user])
// useEffect(() => {
// isMount = true
// return () => {
// isMount = false
// }
// }, [])
return (
<FlatList
// data={getUser}
data={user}
keyExtractor={item => item.id}
renderItem={itemData =>
<MyUser
name={itemData.item.name}
image={itemData.item.imageUri}
details={itemData.item.details.val}
/>
}
/>
)
}
export default TheUser

What is the best way to fetch data in react?

I currently use contentful for data management, so it's not the usual axios/fetch method this time.
I'm using useContext to share the data to my components, and have an useEffect that sets the state with the new data. What is the problem you ask? The ugly syntax. When i pass the data in the second rerender i can't access the data immediately, the data[0][0] doesn't exist yet, so it throws an error. This results in this disgusting syntax: <h5>{data ? data[0].sys.contentType.sys.id : ""}</h5> It might be "fine", and it "works". But it looks absolutely atrocious to me.
App.jsx
const App = () => {
const contentfulHook = useState(null);
useEffect((e) => {
client.getEntries().then((res) => contentfulHook[1](res.items));
/*
OR - same result
(async () => {
const data = await client.getEntries().then((res) => res.items);
contentfulHook[1](await data);
})();
*/
//Remove preloader when content is loaded
setTimeout(() => {
const preloader = document.getElementById("preloader");
preloader.style.opacity = 0;
preloader.addEventListener("transitionend", (e) => {
preloader.remove();
});
}, 0);
}, []);
console.log(contentfulHook[0]);
return (
<contentfulContext.Provider value={contentfulHook}>
<BrowserRouter>
<Global />
<Pages />
</BrowserRouter>
</contentfulContext.Provider>
);
};
render(<App />, document.getElementById("root"));
I'd suggest two things on that matter:
Encapsulate that request logic in a custom hook, so you can remove that code from your component.
Use data, loading and error pattern (as used by Apollo and probably other libraries).
The result would be something like:
const App = () => {
const { data, loading, error } = useRequest();
return (
<contentfulContext.Provider value={data}>
<BrowserRouter>
<Global />
<Pages />
</BrowserRouter>
</contentfulContext.Provider>
);
};
const useRequest = () => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
client.getEntries()
.then((res) => setData(res.items))
.catch((e) => setError(e))
.finally(() => {
removePreloader();
setLoading(false);
});
}, []);
return { data, loading, error };
}
const removePreloader = () => {
setTimeout(() => {
const preloader = document.getElementById("preloader");
preloader.style.opacity = 0;
preloader.addEventListener("transitionend", (e) => {
preloader.remove();
});
}, 0);
}
render(<App />, document.getElementById("root"));
Either way, you still need to check for the data before accessing the data attributes when rendering. Even if it is on a top-level condition like:
const App = () => {
const { data, loading, error } = useRequest();
return (
<contentfulContext.Provider value={data}>
<BrowserRouter>
<Global />
<Pages />
{data && <h5>{data[0].sys.contentType}</h5>}
</BrowserRouter>
</contentfulContext.Provider>
);
};

Categories

Resources