Loader that doesn't render children unless data is fetched - reactjs

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

This is called render prop pattern:
const QueryLoader = ({ isSuccess, children }) => {
return isSuccess ? children() : <h2>Loading</h2>;
};
const Main = () => {
const { isSuccess, data } = fetcher("api");
return (
<QueryLoader isSuccess={isSuccess}>
{() => <div>{data.arrayOfData.innerSomething}</div>}
</QueryLoader>
);
};
For data fetching I hightly recommend using react-query library.

Related

Redux toolkit mutation access to state across multiple components

I'm start using RTQ.
To access query data is possible via generated hook. But for mutations, it doesn't work.
I have this simple example.
This component is responsible for loading "users" from API.
const Page = () => {
const {
isSuccess: isSuccessUsers
isLoading: isLoadingUsers
} = useGetUsersQuery({})
if (isLoadingUsers) return <LoadingSpinner />
if (isSuccessUsers) return <UsersTable />
...
}
This component display users, which are obtained from cache - was filled by the API call in previous component "Page".
Also in that component I want display loading spinner for deleting state.
const UsersTable = () => {
const {
data: dataUsers,
isFetching: isFetchingUsers,
} = useGetUsersQuery({})
const [, {
data: dataDeleteUser,
isLoading: isLoadingDeleteUser,
isSuccess: isSuccessDeleteUser,
isError: isErrorDeleteUser,
error: errorDeleteUser,
}] = useDeleteUserMutation()
useEffect(() => {
if (isSuccess) {
console.log(data.data.message)
}
}, [isSuccess])
useEffect(() => {
if (isError) {
console.log(error.data.message)
}
}, [isError])
if (usersIsFetching || isLoadingDeleteUser) return <LoadingSpinner />
dataUsers.map(user=>(<UsersTableRow user={user}/>))
}
In this component I only want call delete function. But here, when use same pattern just like for query, it doesnt work.
const UsersTableRow = ({user}) => {
const [deleteUser] = useDeleteUserMutation()
const handleDeleUser = () => {
deleteUser(user.id)
}
...
}
I could solve this by pass deleteUser function as prop from UsersTable component to UsersTableRow component.
const [ deleteUser, {
data: dataDeleteUser,
isLoading: isLoadingDeleteUser,
isSuccess: isSuccessDeleteUser,
isError: isErrorDeleteUser,
error: errorDeleteUser,
}] = useDeleteUserMutation()
dataUsers.map(user=>(<UsersTableRow deleteUser={deleteUser} user={user}/>))
I would like to avoid this, because if there are more child components, I will have to pass to every child.
Is there some better solution?
You can use a fixed cache key to "tell" redux-toolkit that you want to use the same result for both components:
export const ComponentOne = () => {
// Triggering `deleteUser` will affect the result in both this component,
// but as well as the result in `ComponentTwo`, and vice-versa
const [deleteUser, result] = useDeleteUserMutation({
fixedCacheKey: 'shared-delete-user',
})
return <div>...</div>
}
export const ComponentTwo = () => {
const [deleteUser, result] = useDeleteUserMutation({
fixedCacheKey: 'shared-delete-user',
})
return <div>...</div>
}

how to pass dynamique data from child to parent in react native

i want to pass the data of text-input from child to parent to submit the dynamic form. when i use useEffect the phone blocked but i don't know why.please can someone help me to solve this problem.thanks to tell me if there are another way to pass the data.
child component
const RenderComponents = ({ sendChildToParent) => {
const [inputsVal, setInputsVal] = useState({});
const handleChange = (name, value) => {
setInputsVal({ ...inputsVal, [name]: value });
};
const senddata = () => {
sendChildToParent(inputsVal);
};
useEffect(senddata);
return (
<>
{getData.length === 0 ? (
<Empty />
) : (
getData.map((item, index) => {
switch (item.type) {
case "TextInput":
return (
<>
<InputText
onChangeText={(text) => handleChange(item.nameC, text)}
ModuleName={item.nameC}
placeholder={item.options.placeholder}
required={item.options.required}
key={index}
/>
</>
);
case "Phone":...
Parent Component
export function TemplateScreen(props) {
const navigation = useNavigation();
const [getData, setData] = React.useState(Mydata);
const [childData, setChildData] = useState([]);
const sendChildToParent = (dataFromChild) => {
setChildData(dataFromChild);
};
//*************************************Child Componenet*************** */
const RenderComponents = () => {
const [userTeam, setUserTeam] = useState({});
[...other code here...];
**********Parent Component*******
return (
<ScrollView>
<RenderComponents />
<Button
title="Submit"
onPress={()=>null}
/>...
The structure of your parent component is fine. The issues are in your child component, in the following lines:
const RenderComponents = ({ sendChildToParent) => {
const [inputsVal, setInputsVal] = useState({});
const handleChange = (name, value) => {
setInputsVal({ ...inputsVal, [name]: value });
};
const senddata = () => {
sendChildToParent(inputsVal);
};
useEffect(senddata);
it's not good practice to duplicate the input value in local state. Pass the value down from the parent component as well as the setter function.
you're not passing a dependency array to your useEffect function, so it runs on every render of the component. This sets off the following chain of events:
the parent renders
the child renders
useEffect runs, setting the value of the state in the parent
the parent re-renders
This is an endless loop and what causes your app to lock.
there's no need to wrap the state setting functions in your own functions unless you are planning to do additional work there later. There's also no need to run those functions in your component lifecycle (useEffect), because they will run when the input changes.
missing bracket in the first line.
You could rewrite the components in the following way:
// parent component
export function TemplateScreen(props) {
const navigation = useNavigation();
const [getData, setData] = React.useState(Mydata);
const [childData, setChildData] = useState({});
return (
<ScrollView>
<RenderComponents childData={childData} setChildData={setChildData} />
...
// child component
const RenderComponents = ({ childData, setChildData }) => {
const handleChange = (name, value) => {
setChildData({ ...childData, [name]: value });
};
return (
...

Child component not being updated with funcional components

I have a component where I get repositories like below:
useEffect(() => {
loadRepositories();
}, []);
const loadRepositories = async () => {
const response = await fetch('https://api.github.com/users/mcand/repos');
const data = await response.json();
setUserRepositories(data);
};
return (
<Repository repositories={userRepositories} />
)
The child component only receives the repositories and prints them. It's like that:
const Repository = ({ repositories }) => {
console.log('repository rendering');
console.log(repositories);
const [repos, setRepos] = useState(repositories);
useEffect(() => {
setRepos(repositories);
}, [ ]);
const getRepos = () => {
repos.map((rep, idx) => {
return <div key={idx}>{rep.name}</div>;
});
};
return (
<>
<RepositoryContainer>
<h2>Repos</h2>
{getRepos()}
</>
);
};
export default Repository;
The problem is that, nothing is being displayed. In the console.log I can see that there're repositories, but it seems like the component cannot update itself, I don't know why. Maybe I'm missing something in this useEffect.
Since you're putting the repository data from props into state, then then rendering based on state, when the props change, the state of a Repository doesn't change, so no change is rendered.
The repository data is completely handled by the parent element, so the child shouldn't use any state - just use the prop from the parent. You also need to return from the getRepos function.
const Repository = ({ repositories }) => {
const getRepos = () => {
return repositories.map((rep, idx) => {
return <div key={idx}>{rep.name}</div>;
});
};
return (
<>
<RepositoryContainer>
<h2>Repos</h2>
{getRepos()}
</>
);
};
export default Repository;
You can also simplify
useEffect(() => {
loadRepositories();
}, []);
to
useEffect(loadRepositories, []);
It's not a bug yet, but it could be pretty misleading - your child component is named Repository, yet it renders multiple repositories. It might be less prone to cause confusion if you named it Repositories instead, or have the parent do the .map instead (so that a Repository actually corresponds to one repository array item object).
Change this
useEffect(() => {
setRepos(repositories);
}, [ ]);
to
useEffect(() => {
setRepos(repositories);
}, [repositories]);

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>
));
};

memoized a closure and render callback with useCallback

let's I have a graphql mutation component, that I reuse in many places
const MarkAsViewed =({ type = 1, children }) => {
const markAsViewed = (commitMutation) => (type) => {
return commitMutation({
variables: { type }
});
};
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{
(commitMutation, { error, loading }) => children({
markAsViewed: markAsViewed(commitMutation)
})
}
</MarkAsViewedMutation>
);
};
however since markAsViewed is a closure function, it will always return different function with different ref which means different for react.
this makes the child component to have to do a useCallback like:
const alwaysSameRefFunc = useCallback(()=>{ markAsViewed(), []}
above works but creates 2 problems:
I get linter warning saying I should add markAsViewed as dependency blah blah. which I cannot, because it triggers infinite loop (since it's different ref every time)
everyone that uses <MarkAsViewed /> component will need to manually memoirzation
Ideally this is what I want, but it's an invalid code, because "markAsViewed" is not a react component and cannot have useCallback
const markAsViewed = (commitMutation) => useCallback((type) => {
return commitMutation({
variables: { type }
});
}, []);
any idea how can I solve the issue?
note: we are not ready to update Apollo version to have hoook yet
Does the following work?
const markAsViewed = commitMutation => type => {
return commitMutation({
variables: { type },
});
};
const MarkAsViewed = ({ type = 1, children }) => {
const fn = useCallback(
(commitMutation, { error, loading }) =>
children({
markAsViewed: markAsViewed(commitMutation),
}),
[children]
);
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{fn}
</MarkAsViewedMutation>
);
};
I'm not sure if that will work because it still depends on children, if that causes unnesesary renders then maybe post how MarkAsViewed component is rendered.

Resources