I am using react hooks in my project and I want to show text 'loading' until I get a response from all requests in current page. How to do this?
There are a number of ways to do this but here is one pattern I tend to use:
const ComponentMakingApiCall = () => {
const [ isLoading, setIsLoading ] = useState(true)
useEffect(() => {
axios.post({
// whatever request you are making
}).then(response => {
setIsLoading(false)
})
},[])
if (isLoading){
return <h1>Loading...</h1>
} else {
return (
<div>
// render some content
</div>
)
}
}
If you have multiple requests, you could do something like this:
const ComponentMakingApiCall = () => {
const [ requestALoading, setRequestALoading ] = useState(true)
const [ requestBLoading, setRequestBLoading ] = useState(true)
useEffect(() => {
axios.post({
// request A
}).then(response => {
handleResponse(response)
setRequestALoading(false)
})
},[])
useEffect(() => {
axios.post({
// request B
}).then(response => {
handleResponse(response)
setRequestBLoading(false)
})
},[])
if ( requestALoading || requestBLoading ) {
return <h1>Loading...</h1>
} else {
return (
<div>
// render some content
</div>
)
}
}
const [isLoading, setIsLoading] = useState(false);
async () => {
setIsLoading(true)
// your codes (fetch API, try/catch, etc)
setIsLoading(false)
}
return (
<React.Fragment>
{isLoading && <LoadingComponentUI />}
{!isLoading && yourCondition && (
<YourComponent />
)}
</React.Fragment>
);
Related
Here's the scenario. I have a app and a search component.
const App = () => {
const [search, setSearch] = useState("initial query");
// ... other code
return (
<Search search={search} setSearch={setSearch} />
...other components
);
};
const Search = ({ search, setSearch }) => {
const [localSearch, setLocalSearch] = useState(search);
const debouncedSetSearch = useMemo(() => debounce(setSearch, 200), [setSearch]);
const handleTextChange = useCallback((e) => {
setLocalSearch(e.target.value);
debouncedSetSearch(e.target.value);
}, [setLocalSearch]);
return (
<input value={localSearch} onChange={handleTextChange} />
);
}
It's all good until this point. But I want to know what's the best way to change the search text on an external event. So far, the best approach I've found is using events.
const App = () => {
const [search, setSearch] = useState("initial query");
// ... other code
useEffect(() => {
onSomeExternalEvent((newSearch) => {
setSearch(newSearch);
EventBus.emit("updateSearch", newSearch);
});
}, []);
return (
<Search search={search} setSearch={setSearch} />
...other components
);
};
const Search = ({ search, setSearch }) => {
const [localSearch, setLocalSearch] = useState(search);
const debouncedSetSearch = useMemo(() => debounce(setSearch, 200), [setSearch]);
const handleTextChange = useCallback((e) => {
setLocalSearch(e.target.value);
debouncedSetSearch(e.target.value);
}, [setLocalSearch]);
useEffect(() => {
EventBus.subscribe("updateSearch", (newSearch) => {
setLocalSearch(newSearch);
});
}, []);
return (
<input value={localSearch} onChange={handleTextChange} />
);
}
Is there a better (correct) way of doing this?
I am trying to fetch employees and here is what I am trying to do using useEffect
function AdminEmployees() {
const navigate = useNavigate();
const dispatch = useDispatch();
// fetching employees
const { adminEmployees, loading } = useSelector(
(state) => state.adminFetchEmployeeReducer
);
useEffect(() => {
dispatch(adminFetchEmployeeAction());
if (adminEmployees === "unAuthorized") {
navigate("/auth/true/false");
}
}, [adminEmployees, navigate,dispatch]);
console.log("Here i am running infinie loop");
console.log(adminEmployees);
return (
<>
{loading ? (
<Loader></Loader>
) : adminEmployees === "no employees" ? (
<h1>No Employees</h1>
) : (
<>
{adminEmployees &&
adminEmployees.map((employee) => {
return (
<div className="admin__employee__container" key={employee.id}>
<AdminSingleEmployee
employee={employee}
></AdminSingleEmployee>
</div>
);
})}
</>
)}
</>
);
}
Here I want to achieve 2 goals:
fetch adminEmployees
if (adminEmployees==='unAuthorized') then go to loginPage
but when doing this as in the code, it creates infinite loop.
How can I achieve the desired functionality?
Easy dirty path: split useEffect into 2
useEffect(() => {
if (adminEmployees === "unAuthorized") {
navigate("/auth/true/false");
}
}, [adminEmployees, navigate]);
useEffect(() => {
dispatch(adminFetchEmployeeAction());
}, [dispatch]);
Better way: handle that case in reducer or action creator to flip flag in the store and then consume it in component:
const { shouldNavigate } = useSelector(state => state.someSlice);
useEffect(() => {
if(shouldNavigate) {
// flipping flag back
dispatch(onAlreadyNavigated()));
navigate("/yourPath...");
},
[navigate, dispatch, shouldNavigate]
);
I am fetching an API data set and filtering that data with a search bar to locate by first or last name. I also have an input field that allows you to add "tags" to the data set that I am mapping through. I am trying to add a second search bar to filter the original data by the unique tags as well, but can not figure out how to incorporate that information into the filter.
export default function Home() {
const [students, setStudents] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
const [showTests, setShowTests] = useState({});
const [tagSearch, setTagSearch] = useState("");
const [tags, setTags] = useState([]);
useEffect(() => {
const getData = async () => {
try {
const response = await axios.get(
<!-- API -->
);
setStudents(response.data);
setError(null);
} catch (err) {
setError(err.message);
setStudents(null);
} finally {
setLoading(false);
}
};
getData();
}, []);
return (
<div className="home-main">
<Search setSearch={setSearch} />
<TagSearch setTagSearch={setTagSearch} />
{loading && <div>Loading, please wait ...</div>}
{error && (
<div>{`An Error has occurred. - ${error}`}</div>
)}
<div className="students">
<Fragment>
{
students
&&
students.students.filter((val) => {
if(search === '' || tagSearch === '') {
return val
} else if(val.firstName.toLowerCase().includes(search.toLowerCase())
|| val.lastName.toLowerCase().includes(search.toLowerCase())
|| tags.text.toLowerCase().includes(tagSearch.toLowerCase()) ){
return val
}
}).map(({val}) => (
<!-- additional info -->
<div className="tags">
<Tags setTags={setTags} />
</div>
</div>
</div>
))
}
</Fragment>
</div>
</div>
);
}
This is where the "tag" state is coming from...
export default function Tags({setTags}) {
const [inputText, setInputText] = useState('');
const [tiles, setTiles] = useState([]);
const inputTextHandler = (e) => {
setInputText(e.target.value);
};
const submitTagHandler = () => {
setTiles([
...tiles, {text: inputText, id: Math.floor(Math.random() * 1000000)}
]);
setTags([
...tiles, {text: inputText}
])
setInputText('');
};
return (
<div className="tags-main">
<div className="tiles-contain">
{
tiles.map((obj) => (
<Tiles key={obj.id} text={obj.text} id={obj.id} tiles={tiles} setTiles={setTiles} />
))
}
</div>
<input value={inputText} onChange={inputTextHandler} onKeyPress={(e) => {
if(e.key === 'Enter') {
if(inputText !== "") {
submitTagHandler();
} else {
alert("Please enter a tag")
}
};
}} placeholder='Add Tag Here' type="text" />
</div>
);
}
It works without the tag state added to the filter. After adding the tag logic neither search bar works. How can I add the array of tags to the filter dependency to sort by first or last name and tags?
I'm pretty sure you were getting an error "cannot read toLowerCase of undefined"
You probably wanted to do something like this
tags.some(tag => tag.text.toLowerCase() === tagSearch.toLowerCase())
or
tags.map(tag => tag.text.toLowerCase()).includes(tagSearch.toLowerCase())
I am just learning about Apollo-React but I couldn't make graphql request
This is how I do without Apollo
const Search = () => {
const [searchedText, setSearchedText] = React.useState('')
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
const debounceHandler = (searchedText) => debounce(() => {
sendQuery(`{search(str:"${searchedText}") {name}}`).then(({search}) => {
if (!search) return
setSuggestions(search)
})
}, 500)
const handleInputChange = async (e) => {
if(e.key === 'Enter') {
const name = e.target.value
sendQuery(`{getPokemon(str:"${name}"){name, image}}`).then(({getPokemon}) => {
setSelected(getPokemon)
})
}
debounceHandler(searchedText)()
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onChange={(e) => setSearchedText(e.target.value)} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
)) }
</div>
</div>
)
}
Now without my own sendQuery function, I want to use Apollo's useQuery hook.
const GET_POKEMON = gql`
query getPokemon ($str: String!) {
getPokemon(str: $str) {
name
image
}
}
`;
const SEARCH = gql `
query search($str: String!) {
search(str:$str) {
name
}
}
`;
These are my queries and results correctly on the playground. Now I write Search function again. I say whenever searchedText changes (WHen user types in), query Search and set the returning data as suggestions. Whenever user hits enter, I want to query the Pokemon from backend and set it as selected.
const Search = () => {
const [searchedText, setSearchedText] = React.useState(null)
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
React.useEffect(() => {
const { data } = useQuery(SEARCH, {
variables: { "str": searchedText },
pollInterval: 500,
});
if (data) {
setSuggestions(data)
}
}, [searchedText])
const fetchAndSelect = name => {
setSearchedText('')
const { pokemon } = useQuery(GET_POKEMON, {
variables: {
"str": name
}
})
setSelected(pokemon)
}
const handleInputChange = (e) => {
const name = e.target.value
if(e.key === 'Enter') {
return fetchAndSelect(name)
}
setSearchedText(name)
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
))}
</div>
</div>
)
}
But this gives Invalid hook call error. If I don't make the query inside useEffect ( I am not sure what is wrong with this?) this time I get Rendered more hooks than during the previous render. error. I am not sure what I am doing wrong?
EDIT
Based on answer I edit the code like following
const Search = () => {
const [searchedText, setSearchedText] = React.useState(null)
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
const debouncedSearch = debounce(searchedText, 1000) // Trying to debounce the searched text
const [searchPokemons, { data }] = useLazyQuery(SEARCH);
const [getPokemon, { pokemon }] = useLazyQuery(GET_POKEMON)
React.useEffect(() => {
if (!searchedText) return
setSelected(null)
searchPokemons({ variables: { str: searchedText }})
if (data) {
console.log(data)
setSuggestions(data)
}
}, [debouncedSearch])
const fetchAndSelect = name => {
setSearchedText('')
getPokemon({variables: {str: name}})
if (pokemon) {
setSelected(pokemon)
}
}
const handleInputChange = (e) => {
const name = e.target.value
if(e.key === 'Enter') {
return fetchAndSelect(name)
}
setSearchedText(name)
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
))}
</div>
</div>
)
}
I am unable to type anything on the input. It is fetching like crazy. Please help
You should use useLazyQuery Hook in this case. It is very useful for things that happen at an unknown point in time, such as in response to a user's search operation.
How about If you call use your hook on the top of your function and just call it inside the useEffect hook.
const [search, { data }] = useLazyQuery(SEARCH, {
variables: { "str": searchedText },
pollInterval: 500,
});
React.useEffect(() => {
if (searchedText)
search() // Function for executing the query
if (data)
setSuggestions(data)
}, [searchedText])
As you see, useLazyQuery handles fetching data in a synchronous way without any promises.
I'm new to React. I'm trying to make my socket io listener work. When I it out of useEffect it works but it is called several times. In useEffect it is called only once (which is good obviously) but this time users are not updated - initial value.
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
getUsers();
if(users.length > 0) {
socket.on("statusChange", (data) => {
console.log(users); // this returns initial state of users
let tempUsers = [...users];
let ndx = tempUsers.findIndex(obj => obj.id === data.id);
if(ndx === -1)
return;
tempUsers[ndx].status = data.status;
setUsers(tempUsers);
});
}
}, []);
function getUsers() {
fetch(/* stuff */)
.then(res => res.json())
.then(res => setUsers(res.data));
}
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;
Some parts where I'm making assumptions:
getUsers() should be called only once: when the component mounts
We want to listen to socket.on("statusChange") and get updates about users we got from getUsers().
// This is pulled outside the component.
function getUsers() {
fetch(/* stuff */)
.then(res => res.json());
}
function Users() {
const [users, setUsers] = useState([]);
// Fetch users only once, when the component mounts.
useEffect(() => {
getUsers().then(res => {
setUsers(res.data);
});
}, []);
// Listen to changes
useEffect(() => {
if (!users.length) return;
const listener = (data) => {
// Functional update
// See: https://reactjs.org/docs/hooks-reference.html#functional-updates
setUsers(prevUsers => {
let nextUsers = [...prevUsers];
let ndx = nextUsers.findIndex(obj => obj.id === data.id);
// What's up here? See below.
if(ndx === -1) return nextUsers;
nextUsers[ndx].status = data.status;
return nextUsers;
});
};
socket.on("statusChange", listener);
// Unsubscribe when this component unmounts
// See: https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
return () => socket.off("details", listener);
}, [users]);
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;
About if(ndx === -1) return nextUsers;
This means that users will never change in size, i.e. you won't handle data about a new user.
Alternatively, you could do if(ndx === -1) return [ ...nextUsers, data ];
Instead of simply executing getUsers, use it with a callback. And then in the callBack execute setUsers:
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch(/* stuff */)
.then(res => res.json())
.then(res => {
if(res.data.length > 0) {
socket.on("statusChange", (data) => {
console.log(res.data); // this returns initial state of users
let tempUsers = [...res.data];
let ndx = tempUsers.findIndex(obj => obj.id === data.id);
if(ndx === -1)
return;
tempUsers[ndx].status = data.status;
setUsers(tempUsers);
});
}
});
}, []);
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;