React Child Component Api Calling Two Times (strict mode is off) - reactjs

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.

Related

useEffect to rerender this component

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

React useEffect not running after state change

I am querying Firebase real time database, saving the result into state and then rendering the results.
My data is not displaying because the page is rendering before the data is had. What I don't understand is why
useEffect(() => {
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
}, [projects]);
My use effect is not running once the projects state change is triggered, populating the array and triggering the conditional render line.
What am I missing here?
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const result = [];
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
useEffect(() => {
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
}, [projects]);
return (
<>
{result.length > 0 && result}
</>
);
};
result should also be a state!
Right now at every rerender result is being set to []. So when the useEffect does kick in, the subsequent rerender would set result to [] again.
This should not be a useEffect. Effects run after rendering, but you're trying to put <Project> elements on the page, which must happen during rendering. Simply do it in the body of your component:
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
const result = [];
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
return (
<>
{result.length > 0 && result}
</>
);
result.push does not mutate result in place. It instead creates a copy of the array with the new value.
As a solution, you could get your current code working by hoisting result into a state variable like so:
const [result, setResult] useState([])
...
useEffect(() => {
for (var key in projects) {
...
setResult([...result, <Project props={projectData} />])
}
}, [result, projects]);
however, this solution would result in an infinite loop...
My suggestion would be to rework some of the logic and use projects to render your Project components, instead of creating a variable to encapsulate your render Components. Something like this:
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
return (
<>
{projects.length > 0 && projects.map(project=>{
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
return <Project props={projectData} />
})}
</>
);
};
You're component is not re-rendering since react doesn't care about your result variable being filled.
Set it up as a state like this: const [result, setResult] = useState([]);
Then use map to return each item of the array as the desire component:
{result.length > 0 && result.map((data, index) => <Project key={index} props={data} />)}

First time input doesn't get submitted in React

I'd like make an API call, which user input makes part of the API URL. Data is only fetched on demand after user submit.
My problem is: after first time input and submit, input is processed as an empty string, constructed wrong URL and made API call. (still loads data but the wrong data)
Only after second submit does it get actual user input, construct the correct URL and display the right data.
monitering network:
User input is stored in enteredWallet, Console.log(enteredWallet) prints the input, but setOwner(enteredWallet) doesn't change owner to be enteredWallet.
import { useState } from "react";
// example input: 0x147412d494731cbb91dbb5d7019464a536de04dc
function App() {
const [data, setData] = useState([]);
const [enteredWallet, setEnteredWallet] = useState("");
const [owner, setOwner] = useState("");
const walletChangeHandler = (event) => {
setEnteredWallet(event.target.value);
};
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
fetchNFTHandler();
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
};
function fetchNFTHandler() {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${owner}&order_direction=desc&offset=0&limit=10`
)
.then((res) => {
return res.json();
})
.then((data) => {
const transformedData = data.assets.map((element, index) => {
return {
title: element.name,
id: index,
};
});
setData(transformedData);
console.log("fetched");
});
}
return (
<div className="App">
<header className="App-header">
<h3>Show me assets in this wallet</h3>
<form onSubmit={submittedHandler}>
<input
placeholder="wallet address"
value={enteredWallet}
onChange={walletChangeHandler}
/>
<button>Submit</button>
</form>
<div>
{data.map((element) => (
<li key={element.id}>{element.title}</li>
))}
</div>
</header>
</div>
);
}
export default App;
Because owner in fetchNFTHandler doesn't update immediately after call setOwner.
Why don't use onwer as a param.
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
fetchNFTHandler(enteredWallet); //here
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
};
function fetchNFTHandler(owner) {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${owner}&order_direction=desc&offset=0&limit=10`
)...
or if you need use it as state indeed.
use useEffect to call fetchNFTHandler
useEffect(() => {
fetchNFTHandler();
}, [owner]) // when owner change, fetchNFTHandler will be call
If you want use a variable, it can take effect at once. you can try useRef.
const ownerRef = useRef("");
const submittedHandler = (event) => {
event.preventDefault();
ownerRef.current = enteredWallet;
fetchNFTHandler();
};
function fetchNFTHandler(owner) {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${ownerRef.current}&order_direction=desc&offset=0&limit=10`
)...
The function returned by useState (in your case, setEnteredWallet or setOwner) is not synchronous. The state is not immediately changed after calling either it. If you want to call fetchNFTHandler every time enteredWallet changes, you can use useEffect. Or simply, you can pass enteredWallet to fetchNFTHandler as a parameter. An example usage of useEffect:
useEffect(() => {
fetchNFTHandler();
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
}, [owner, enteredWallet]) // Call method above when owner or enteredWallet change
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
// You don't need the following lines anymore
// fetchNFTHandler();
// console.log("enteredWallet:", enteredWallet);
// console.log("owner:", owner);
};

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