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>
Related
Here, are three components User Details and its two Childs are UserSpecificData1 and UserSpecificData2.
In User Details component im getting User Details with userId by api calling.
Now i declared Two childs by passing that user id.
Problem is: Two child api is calling two times! Why? React strict mode is off.
Note: I noticed that child components are rendering two times by console.log
`
export const UserDetails = () => {
const params = useParams(); // {userId: 223}
useEffect(() => {
if(params?.userId){
getCustomerDetails(params.userId) // 223
}
}, [params.userId]);
return (
<div>
<UserSpecificData1 userId={params.userId}/>
<UserSpecificData2 userId={params.userId}/>
</div>
);
};
// Component 1
const UserSpecificData1 = props => {
const [currentPage, setCurrentPage] = useState(0);
const [filteredBy, setFilteredBy] = useState({});
const [sortBy, setSortBy] = useState('ASC');
useEffect(() => {
getSpecificDataOne({
id: props.userId, //223
filteredBy: filteredBy,
page: currentPage,
size: 10,
sortBy: sortBy,
})
}, [sortBy, currentPage, filteredBy]);
return <div>
</div>
};
// Component 2
const UserSpecificData2 = props => {
const [currentPage, setCurrentPage] = useState(0);
const [filteredBy, setFilteredBy] = useState({});
const [sortBy, setSortBy] = useState('ASC');
useEffect(() => {
getSpecificDataTwo({
id: props.userId, //223
filteredBy: filteredBy,
page: currentPage,
size: 10,
sortBy: sortBy,
})
}, [sortBy, currentPage, filteredBy]);
return <div>
</div>
};
`
Hey i just reviewed your code and i came up with conclusion that you have to add a condition on both child useEffect where api is being called and check for prop.userId exist or not and don't forgot to passed it as dependency array.
useEffect(()=>{
if(props?.userId){
getSpecificDataTwo({
id: props.userId, //223
filteredBy: filteredBy,
page: currentPage,
size: 10,
sortBy: sortBy,
});
}
},[sortBy, currentPage, filteredBy,props.userId]);
let me know if this works for you otherwise we will go for another way.
My guess is that the code isn't quite complete?
So I'm assuming you also have a [content, setContent] somewhere in the first component UserDetails - and if so, it'll first render the child components, and then, if params.userId exists, after the content has loaded it'll re-render.
A couple of ways to stop this, probably the best being surrounding your child components with { content && <Child 1 />...}
So complete code would be:
export const UserDetails = () => {
const params = useParams(); // {userId: 223}
const [content, setContent] = useState(null)
useEffect(() => {
if(params?.userId){
getCustomerDetails(params.userId)
.then(result => {
setContent(result);
}) // 223
}
}, [params.userId]);
return (
<div>
{ content &&
<>
<UserSpecificData1 userId={params.userId}/>
<UserSpecificData2 userId={params.userId}/>
</>
}
</div>
);
};
Personally I'd probably also put the userId into a hook and use that as the check, up to you which works better.
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
Example to make the context clear:
I am trying to render a component with two sets of data coming from API calls. I am also returning early if the first API call fails. The second API call depends on the data of the first API result. I don't want to combine both effects because that would mean the whole of component does not render till I get bot API results.
This is the psuedo code
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
callApi1();
}, []);
// early return so that all the complex logic below is not called on ever render
if (!dataFromEffect1) return <div>No Data1</div>;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
useEffect(() => {
const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
callApi2();
}, [data1]);
return (
<div>
{/* no need to null check here, because of the early return on top */}
{dataFromEffect1}
{/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
{dataFromEffect2 ? (
<div>
{dataFromEffect2}
</div>
) : null}
</div>
);
};
Problem
The above code does not work because you cannot add a useEffect conditionally (the early return messes it up)
Trying to find the best workaround for this problem.
Just creat a component for dataFromEffect2:
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () =>
setDataFromEffect1(await (await fetch("/api1")).json());
callApi1();
}, []);
if (!dataFromEffect1) return <div>No Data1</div>;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
return (
<div>
{dataFromEffect1}
<Component data1={data1} dataFromEffect2={dataFromEffect2} setDataFromEffect2={setDataFromEffect2} />
</div>
);
};
const Component = ({ data1, dataFromEffect2, setDataFromEffect2 }) => {
useEffect(() => {
const callApi2 = async () =>
setDataFromEffect2(
await (await fetch("/api2", { headers: data1 })).json()
);
callApi2();
}, [data1]);
return <div>{dataFromEffect2}</div>;
};
You can try something like this. It will kill both useEffects, but it will not run the second one unless it retrieved data from the first. Also, I did not fix this in your code but you should not use async code within useEffect. This can lead to memory leaks and unnexpected bugs. You are also not cleaning up the fetch from within the useEffect. Academind has a nice blog explaining how to fix this and what will happen if you keep the code like this https://academind.com/tutorials/useeffect-abort-http-requests/
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
callApi1();
}, []);
useEffect(() => {
if(!dataFromEffect1.length) return;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
callApi2();
}, [dataFromEffect1]);
// early return so that all the complex logic below is not called on ever render
if (!dataFromEffect1) return <div>No Data1</div>;
return (
<div>
{/* no need to null check here, because of the early return on top */}
{dataFromEffect1}
{/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
{dataFromEffect2 ? (
<div>
{dataFromEffect2}
</div>
) : null}
</div>
);
};
Two options ( I went with the second one for now):
create a separate component for abstracting the second useEffect as suggested by #viet in the answer below.
move the useEffect above the early return, but I will have to duplicate the if conditions inside the effect. As described by in the answer below.
In my app I have profile section with a form. When the component mounts I want to fetch user data from firebase, and display it in the form, with the current values of the user profile. Either using the "value" prop or the "placeholder" prop.
When the user makes changes in the form inputs and submit the changes, I want the database to update and the form to update with the new data.
Currently I can make the database value appear in the form input field, or I can make the form input field empty, but update the database. But not both.
The following code makes the database data render in the form input, but it cant be changed.
I know it could be something with the second useEffect() and the getUserData() function, that I cant seem to figure out.
const UserEdit = (props) => {
const [currentUser, setCurrentUser] = useState('');
const [forening, setForening] = useState('');
useEffect(() => {
firebase_app.auth().onAuthStateChanged(setCurrentUser);
}, [])
const getUserData = async () => {
await dbRef.ref('/' + currentUser.uid + '/profil/' ).once('value', snapshot => {
const value = snapshot.val();
setForening(value)
})
}
useEffect(() => {
getUserData()
},[] )
const handleInput = (event) => {
setForening(event.target.value)
}
const updateUserData = () => {
dbRef.ref('/' + currentUser.uid + '/profil/' ).set({foreningsnavn: forening}, function(error) {
if(error) {
console.log("update failed")
} else {
alert(forening)
}
})
}
const handleClick = () => {
updateUserData()
}
return (
<>
<div className="card-body">
<div className="row">
<div className="col-md-5">
<div className="form-group">
<label className="form-label">{Forening}</label>
<input className="form-control" type="text" value={forening} onChange={handleInput}/>
</div>
</div>
</div>
</div>
</>
)
}
Your second useEffect will run only one time because the second argument array [] of dependencies is empty:
useEffect(() => {
getUserData()
},[] )
You can add foreign dependency to make useEffect run with input change
useEffect(() => {
getUserData()
},[foreign] )
or you can use polling to sync database state
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