Suppose I have a list of items I would like to render and select (like a Todo app).
I'd like to keep the selection logic inside custom react hook and have items live somewhere else in local state.
Now, I would like to update the selection list, kept in the custom hook, whenever I fetch some more items. For this task I am passing data as parameter to selection hook and I am using useEffect to update the selection:
import { useEffect, useState } from "react";
const itemsArrayToObject = (items) =>
Object.fromEntries(items.map((i) => [i.id, { ...i, selected: false }]));
export function useSelection({ data }) {
const [selection, setSelection] = useState(itemsArrayToObject(data));
useEffect(() => {
setSelection((selection) => {
return {
...itemsArrayToObject(data),
...selection
};
});
}, [data]);
const isSelected = (itemId) => selection?.[itemId]?.selected ?? false;
const toggle = (itemId) => {
setSelection((s) => {
const item = s[itemId];
return {
...s,
[itemId]: {
...item,
selected: !item.selected
}
};
});
};
return {
isSelected,
toggle
};
}
This almost works but the problem is if I want to synchronize two things: fetching data and toggling items. Eg.
const onLoadAndToggle = async () => {
await load();
toggle(0);
};
load is a async function that fetches the data. It also triggers state update so that data is updated and the selection can be updated inside useSelection hook.
Example how it all can work:
const [data, setData] = useState([]);
const addItems = (items) => {
setData((state) => [...state, ...items]);
};
const { load } = useFetch({ addItems });
const { isSelected, toggle } = useSelection({ data });
const onLoadAndToggle = async () => {
await load();
toggle(0);
};
Now, the problem is that when calling toggle(0) my custom hook has a stale selection, even when using setState(state => ... singature.
It is because the whole fetching and updating data in state takes too long.
I can see some ugly ways to solve that problem but I wonder what would be the elegant or idiomatic react way to solve that.
I have made a code sandbox, if it helps: https://codesandbox.io/s/selection-fetch-forked-nyl0kt?file=/src/App.js:376-512
Try clicking "Load and toggle first" first to see how the app crashed because the selection is not yet updated.
What you need is to initialize toogled items from the code itself. We can do this by providing the id's of the items that we want to toggle to the hook itself.
Updated hook -
const itemsArrayToObject = (items, itemsToggled) => {
if (Array.isArray(itemsToggled)) {
return Object.fromEntries(
items.map((i) => [i.id, { ...i, selected: itemsToggled.includes(i.id) }])
);
}
return Object.fromEntries(
items.map((i) => [i.id, { ...i, selected: false }])
);
};
export function useSelection({ data }, itemsToggled) {
const [selection, setSelection] = useState(
itemsArrayToObject(data, itemsToggled)
);
useEffect(() => {
setSelection((selection) => {
return {
...itemsArrayToObject(data, itemsToggled),
...selection
};
});
}, [data, itemsToggled]);
Now call to hook becomes -
const { isSelected, toggle } = useSelection({ data }, [0, 1]);
Updated codesandbox
This also decouples loading data & toggling of an item initially.
Related
I have a websocket server that sends an object containing some hashes every 15 seconds. When the client receives a hash, I want to check with my current hash. If they differ, I want to make a call to an API to fetch new data.
The socket is working and sending the hash correctly. If the data updates on the server I get a different hash. My problem is that the hash variable I use to store the current hash is not updated correctly.
I have disabled the socket listening in my component, just to make sure that that is not the problem. Instead I have added a setInterval to mimik the socket update.
This is my code (socked code disabled but left as a comment):
import { useCallback, useEffect, useState } from "react";
import { useAuth, useSocket } from "../utils/hooks";
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState<QuestionListModel[]>([]);
const { user } = useAuth();
const { socket } = useSocket();
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response: ApiResponse | boolean = await getQuestionLists(user?.token);
if (typeof response !== "boolean" && response.data) {
setQuestionLists(response.data);
}
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = useCallback((newHash: Hash) => {
console.log("called update");
let shouldUpdate = false;
let originalHash = { ...hash };
let updatedHash = { ...newHash };
console.log("new: ", newHash);
console.log("stored: ", originalHash);
if (hash.questionList !== newHash.questionList) {
console.log("was not equal");
updatedHash = { ...updatedHash, questionList: newHash.questionList}
shouldUpdate = true;
}
if (shouldUpdate) {
console.log("trying to set new hash: ", updatedHash);
setHash(updatedHash);
fetchQuestionLists();
}
}, [hash]);
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, 15000)
}, []);
return (
<>
... Things here later ...
</>
);
};
export default Admin;
After the initial render, and waiting two interval cycles, this is what I see in the console:
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
You can see that stored is empty. That leads me to believe that setHash(updatedHash); never runs for some reason. Why is that?
Having hacked about with this in codepen here: https://codesandbox.io/s/as-prop-base-forked-l3ncvo?file=/src/Application.tsx
This seems to me to be a closure issue as opposed to a React issue. If you have a look in the dev tools, you'll see the state of the component is doing what you're expecting it to. The issue is that the console log is not.
useEffect is only ever going to use an old version of update, so the console won't log what you're expecting. If you add update to the dependency array (and add a clean up so we don't end up with tonnes of intervals) you'll get what you're looking for. Can be seen in the linked codepen.
I think the issue in on this line :
socket?.on('aHash', (hash) => update(hash));
maybe when you register a listener, it keeps the first value of update only,
can you please share useSocket?
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
Include setHash in your dependency list et voilà
EDIT: Or well, you should include these dependencies in all your useCallback/useEffect hooks since the reference will be lost whenever the component updates. You always have to include all dependencies in the dependency list not to get unpredictable behavior.
use setState(prevValue => {}) to get the the preferred effect. Also, if you running in a Strict mode this will fire the setState twice.
Here is how the code should look like:
import { useCallback, useEffect, useState } from "react";
import { faker } from '#faker-js/faker';
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState([]);
const [ hash, setHash ] = useState({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response = {data: {hash: 'asdf-1234'}}
setQuestionLists(response.data);
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = (newHash) => {
console.log("called update");
setHash(oldHash => {
console.log('old hash: ', oldHash);
console.log('new hash', newHash);
if (JSON.stringify(oldHash) !== JSON.stringify(newHash)) {
return newHash
}
})
};
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: faker.random.numeric(36)});
}, 15000)
}, []);
return (
<>
<h2>Hash</h2>
{JSON.stringify(hash)}
</>
);
};
export default Admin;
In both cases (socket & interval) the issue is that you need to re-define the callback functions with the new context of the variables in the scope, whenever something changes. In this case you will probably need to put "update" (and whatever other variable you need to "watch") inside the dependancy array of the useEffect.
Ive had a similar issues. Here is how I ended up defining socket callback that updates correctly. Notice that I added the save function (just a function that saves the state into the useState). Also, you need to return a clean up function to turn the socket callback off when the component unmounts. This way every time anything changes in the dependancy array, the hook re-runs and recreates that callback with the new information.
React.useEffect(() => {
socketRef?.current?.on(
'private_message_sent_to_client',
(data: IMessageResult) => {
savePrivateMessages(data);
}
);
return () => {
socketRef?.current?.off('private_message_sent_to_client');
};
}, [meta, selectedChatId, savePrivateMessages]);
And here is an example for you
React.useEffect(() => {
socket?.on('aHash', (hash) => update(hash));
return () => {
socket?.off('aHash')
};
}, [update, hash]);
I am trying to use useEffect to rerender postList (to make it render without the deleted post) when postsCount change, but I can't get it right. I tried to wrap everything inside useEffect but I couldn't execute addEventListener("click", handlePost) because I am using useEffect to wait for this component to mount first, before attaching the evenListener.
Parent component:
function Tabs() {
const [posts, setPosts] = useState([]);
const dispatch = useDispatch();
const postsCount = useSelector((state) => state.posts.count);
useEffect(() => {
document.getElementById("postsTab").addEventListener("click", handlePost);
}, [handlePost]);
const handlePost = async (e) => {
const { data: { getPosts: postData }} = await refetchPosts();
setPosts(postData);
dispatch(postActions.getPostsReducer(postData));
};
const { data: FetchedPostsData, refetch: refetchPosts } = useQuery( FETCH_POSTS_QUERY, { manual: true });
const [postList, setPostsList] = useState({});
useEffect(() => {
setPostsList(
<Tab.Pane>
<Grid>
<Grid.Column>Title</Grid.Column>
{posts.map((post) => (
<AdminPostsList key={post.id} postId={post.id} />
))}
</Grid>
</Tab.Pane>
);
console.log("changed"); //it prints "changed" everytime postCount changes (or everytime I click delete), but the component doesn't remount
}, [postsCount]);
const panes = [
{ menuItem: { name: "Posts", id: "postsTab", key: "posts" }, render: () => postList }
];
return (<Tab panes={panes} />);
}
child/AdminPostsList component:
function AdminPostsList(props) {
const { postId } = props;
const [deletePost] = useMutation(DELETE_POST_MUTATION, {variables: { postId } });
const dispatch = useDispatch();
const deletePostHandler = async () => {
dispatch(postActions.deletePost(postId));
await deletePost();
};
return (
<>
<Button icon="delete" onClick={deletePostHandler}></Button>
</>
);
}
The Reducers
const PostSlice = createSlice({
name: "storePosts",
initialState: {
content: [],
count: 0,
},
reducers: {
getPostsReducer: (state, action) => {
state.content = action.payload;
state.count = action.payload.length
},
deletePost: (state, action) => {
const id = action.payload
state.content = current(state).content.filter((post) => (post.id !== id))
state.count--
}
},
});
Okay, let discuss this in separate comment. Key point is to decouple posts logic from wrapper component(Tabs). You should create component dedicated only to posts and render it in wrapper. Like that you can easily isolate all posts-related logic in posts-related component, for example to avoid attaching some listeners from wrapper(because it is not intuitive what you are doing and who listens for what because button is not in that same component). In separated component you will have only one useEffect, to fetch posts, and you will have one selector(to select posts from redux), and then just use that selection to output content from component.
That part <Tab panes={...} /> was the source of most of your problems, because like that you are forced to solve everything above <Tab../> and then just to pass it, which is not best practice in you case since it can be too complicated(especially in case when you could have multiple tabs). That is why you need to decouple and to create tab-specific components.
This would be an idea of how you should refactor it:
function PostsTab() {
const posts = useSelector((state) => state.posts?.content ?? []);
useEffect(() => {
// Here dispatch action to load your posts
// With this approach, when you have separated component for PostsTab no need to attach some weird event listeners, you can do everything here in effect
// This should be triggered only once
// You can maybe introduce 'loading' flag in your reducer so you can display some loaders for better UX
}, []);
return (
<div>
{/* Here use Tab components in order to create desired tab */}
<Tab.Pane>
<Grid>
<Grid.Column>Title</Grid.Column>
{posts.map((post) => (
<AdminPostsList key={post.id} postId={post.id} />
))}
</Grid>
</Tab.Pane>
</div>
);
}
function Tabs() {
return (
<div>
<PostsTab/>
{/** HERE you can add more tabs when you need to
* Point is to create separate component per tab so you can isolate and maintain tab state in dedicated component
and to avoid writing all logic here in wrapper component
* As you can see there is no need to attach any weird listener, everything related to posts is moved to PostsTab component
*/}
</div>
);
}
Ok, let's discuss what I did wrong for the future reader:
There is no need to use this weird spaghetti
useEffect(() => {
document.getElementById("postsTab").addEventListener("click", handlePost);
}, [handlePost]);
const panes = [
{ menuItem: { name: "Posts", id: "postsTab", key: "posts" }, render: () => postList }
];
for I could've used a <Menu.Item onClick={handleClick}>Posts</Menu.Item> to attach the onClick directly.
I had to use useEffect to monitor posts dependency, but .map() will automatically update its content if the array I am mapping had any changes so there is no need to use it use useEffect in this context.
I think I can use lifting state to setPosts from the child component and the change will trigger .map() to remap and pop the deleted element, but I couldn't find a way to so, so I am using a combination of redux (to store the posts) and useEffect to dispatch the posts to the store than I am mapping over the stored redux element, idk if this is the best approach but this is all I managed to do.
The most important thing I didn't notice when I almost tried everything is, I must update apollo-cache when adding/deleting a post, by using proxy.readQuery
this is how I did it
const [posts, setPosts] = useState([]);
const handlePosts = async () => {
const { data: { getPosts: postData } } = await refetchPosts();
setPosts(postData);
};
const handlePosts = async () => {
const { data } = await refetchPosts();
setPosts(data.getPosts);
};
// Using useEffect temporarily to make it work.
// Will replace it with an lifting state when refactoring later.
useEffect(() => {
posts && dispatch(postsActions.PostsReducer(posts))
}, [posts]);
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
update(proxy) {
let data = proxy.readQuery({
query: FETCH_POSTS_QUERY,
});
// Reconstructing data, filtering the deleted post
data = { getPosts: data.getPosts.filter((post) => post.id !== postId) };
// Rewriting apollo-cache
proxy.writeQuery({ query: FETCH_POSTS_QUERY, data });
},
onError(err) {
console.log(err);
},
variables: { postId },
});
const deletePostHandler = async () => {
deletePost();
dispatch(postsActions.deletePost(postId))
};
Thanks to #Anuj Panwar #Milos Pavlovic for helping out, kudos to #Cptkrush for bringing the store idea into my attention
I have a React Native functional component.
I'm using useEffect to fetch some data from AsycStorage and set it to local state. However, before rendering, I want to do some processing/calculations on this data before I can render it on screen. Where should I be doing this calculation?
My screen looks as follows:
const BasicScreen = ({ data, getPosts }) => {
const [myItems, setItems] = useState([]);
const checkForItems = () => {
var storageItems = AsyncStorage.getItem("MyItems").then((item) => {
if (item) {
return JSON.parse(item);
}
});
setItems(storageItems);
};
useEffect(() => {
async function getItems() {
await checkForItems(); // calling function to get data from storage
}
getItems(); // Local Storage
getPosts(); // Store action
}, []);
return (
<View>
<>
<Text>{JSON.stringify(processedItemsA)}</Text>
<Text>{JSON.stringify(processedItemsB)}</Text>
</>
</View>
);
}
export default BasicScreen;
As you can see, I check for items in AsyncStorage and set that data to local state myItems.
I want to do some mathematical calculations and some conditional logic, for example, separate the data of myItems into two separate categories, and then render that on screen. Something like processedItemsA and processedItemsB. Where should I be doing this processing of data?
processedItemsA = myItems => {
// Some logic
}
processedItemsB = myItems => {
// Some logic
}
I'm not sure about where this logic should go.
Note that this processing is required because apart from storage, I also get some data from redux store, and then process it along with that data.
This way you can achieve this
const [processedItemsA, setProcessedItemsA] = useState({});
const [processedItemsB, setProcessedItemsB] = useState({});
doProcessedItemsA = myItems => {
...
setProcessedItemsA({...data}); // set data after process Item A
}
doProcessedItemsB = myItems => {
...
setProcessedItemsB({...data}); // set data after process Item B
}
const checkForItems = () => {
var storageItems = AsyncStorage.getItem("MyItems").then((item) => {
if (item) {
const parsedItem = JSON.parse(item);
doProcessedItemsA(parsedItem);
doProcessedItemsB(parsedItem);
}
});
// setItems(storageItems); <-- No need to set here -->
};
I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.
What I understand is that you can get/set a state by using
const [myState, setMyState] = React.useState(myStateValue);
So. My component runs some functions based on the url prop :
const playlist = new PlaylistObj();
React.useEffect(() => {
playlist.loadUrl(props.url).then(function(){
console.log("LOADED!");
})
}, [props.url]);
Inside my PlaylistObj class, I have an async function loadUrl(url) that
sets the apiLoading property of the playlist to true
gets content
sets the apiLoading property of the playlist to false
Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :
<div
className={classNames({
'api-loading': playlist.apiLoading
})}
>
But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console.
It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?
I tested
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
//refresh playlist if its URL is updated
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
})
}, [props.playlistUrl]);
And this, but it seems more and more unlogical to me, and, well, does not work.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setPlaylist(playlist); //added this
})
}, [props.playlistUrl]);
I just want my component be up-to-date with the playlist object. How should I handle this ?
I feel like I'm missing something.
Thanks a lot!
I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
setPlaylist(playlist); // <-- this playlist reference doesn't change
})
}, [props.playlistUrl]);
I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.
const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setIsLoading(false);
});
}, [props.playlistUrl]);
Use the isLoading state in the render
<div
className={classNames({
'api-loading': isLoading,
})}
>
I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl)
.then(function() {
console.log("LOADED!");
})
.finally(() => setIsLoading(false));
}, [props.playlistUrl]);
Here you go:
import React from "react";
class PlaylistAPI {
constructor(data = []) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
removeEventListener(fn) {
this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
}
setPlayList(data) {
this.data = data;
this.notif();
}
loadUrl(url) {
console.log("called loadUrl", url, this.data)
}
notif() {
this.listeners.forEach(fn => fn());
}
}
export default function App() {
const API = React.useMemo(() => new PlaylistAPI(), []);
React.useEffect(() => {
API.addListener(loadPlaylist);
/**
* Update your playlist and when user job has done, listerners will be called
*/
setTimeout(() => {
API.setPlayList([1,2,3])
}, 3000)
return () => {
API.removeEventListener(loadPlaylist);
}
}, [API])
function loadPlaylist() {
API.loadUrl("my url");
}
return (
<div className="App">
<h1>Watching an object by React Hooks</h1>
</div>
);
}
Demo in Codesandbox
I have a mystery. Consider the following custom React hook that fetches data by time period and stores the results in a Map:
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(new Map(data.set(period, asyncState)));
}
},
[setData, data, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(periodData => {
updateData(dateRange, makeAsyncData(periodData));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRanges /*, updateData - for some reason when included this triggers infinite renders */]);
return data;
}
The useEffect is being repeatedly triggered when updateData is added as a dependency. If I exclude it as a dependency then everything works / behaves as expected but eslint complains I'm violating react-hooks/exhaustive-deps.
Given updateData has been useCallback-ed I'm at a loss to understand why it should repeatedly trigger renders. Can anyone shed any light please?
The problem lies in the useCallback/useEffect used in combination. One has to be careful with dependency arrays in both useCallback and useEffect, as the change in the useCallback dependency array will trigger the useEffect to run.
The “data” variable is used inside useCallback dependency array, and when the setData is called react will rerun function component with new value for data variable and that triggers a chain of calls.
Call stack would look something like this:
useEffect run
updateData called
setState called
component re-renders with new state data
new value for data triggers useCallback
updateData changed
triggers useEffect again
To solve the problem you would need to remove the “data” variable from the useCallback dependency array. I find it to be a good practice to not include a component state in the dependency arrays whenever possible.
If you need to change component state from the useEffect or useCallback and the new state is a function of the previous state, you can pass the function that receives a current state as parameter and returns a new state.
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
In your example you need the current state only to calculate next state so that should work.
This is what I now have based on #jure's comment above:
I think the problem is that the "data" variable is included in the dependency array of useCallback. Every time you setData, the data variable is changed that triggers useCallback to provide new updateData and that triggers useEffect. Try to implement updateData without a dependecy on the data variable. you can do something like setData(d=>new Map(d.set(period, asyncState)) to avoid passing "data" variable to useCallback
I adjusted my code in the manners suggested and it worked. Thanks!
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(traffic => {
updateData(dateRange, makeAsyncData(traffic));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
}, [dateRanges , updateData]);
return data;
}