Below I am trying to fetch data and use the onInputValue function in my other component called Search. It's working fine after first attempt, but I am getting an empty array in my initial button click
const App = () => {
const [results, setResults] = useState([]);
const onInputValue = async (input) => {
const { data } = await nasa.get('/search', {
params: {
q: input,
},
});
if(!results) {
return;
}
setResults(data.collection.items);
console.log(results);
};
return (
<div>
<Search onInputValue={onInputValue} />
</div>
);
};
import React, { useState} from 'react';
const Search = ({ onInputValue }) => {
const [input, setInput] = useState('');
return (
<div className='input-group mb-3'>
<input
type='text'
className='form-control'
placeholder='To infinity and beyond!'
onChange={(e) => setInput(e.target.value)}
/>
<div className='input-group-append'>
<button
onClick={() => {onInputValue(input)}}
className='btn btn-outline-secondary'
type='button'
>
<i className='fas fa-rocket'></i>
</button>
</div>
</div>
);
};
export default Search;
Below is the result I get.
Please advise
setResults is asynchronous if you want to check results you can use useEffect
const App = () => {
const [results, setResults] = useState([]);
useEffect(() => {
console.log(results);
}, [results])
const onInputValue = async (input) => {
const {data} = await nasa.get('/search', {
params: {
q: input,
},
});
if (!results) {
return;
}
setResults(data.collection.items);
};
return (
<div>
<Search onInputValue={onInputValue} />
</div>
);
};
This is due to asynchronous nature of setResults function - after calling it, the updated value of results will be available on the next component render - in your example you're logging in to console immediately after invoking setResults.
It could be because the setResults is batched and therefore move to the next line.
See useState batch updates.
Also
if(!results) { return; }
Is a bit suspect. ![] === false and !['someValue'] === false
Related
I am trying to reset the state on router.push
I have a header component having a search option, When a user Search Something router.push sends him to '/search/${search}'
So basically on the search Page(SSR)
on Search Component I Put limit and skip as well for loadMore Functionality, when I try to search something from the home page or any other page so it runs smoothly because the search page renders and skips, limit gets reset
Problem - My main problem is that When I try to search for something from Search Page then the skip and limit remain the same, In fact, router.push doesn't reset the state
But when I reload the page the state gets reset. Why is that?
my router.push on header component -
const Header = ({searched }) => {
const [values, setValues] = useState({
search: "",
});
const { search } = values;
const router = useRouter();
useEffect(() => {
setHydrated(true);
// getPage();
// setValues({ ...values, search: searched });
console.log(searched);
}, []);
if (!hydrated) {
return null;
}
return (
<React.Fragment>
<h1 className="d-flex align-items-center mb-3 justify-content-between">
<span className="">Feature Request</span>
<div className="d-flex align-items-center">
<Input
className="search"
placeholder="Search Something..."
value={search}
onChange={(e) => {
setValues({ ...values, search: e.target.value });
}}
/>
<AiOutlineSearch
onClick={() => {
router.push(`/search/${search}`);
}}
size={25}
/>
</div>
</h1>
<p>Your Suggestion Matters</p>
</React.Fragment>
);
}
My Search page -
import React from "react";
import { Button, Card, CardText, CardTitle, Col, Container } from "reactstrap";
import Layout from "../../components/Layout";
import Link from "next/link";
import { useState, useEffect } from "react";
import { searchData } from "../../api/page";
import { getCookie } from "../../api/auth";
import { useRouter } from "next/router";
import InfiniteScroll from "react-infinite-scroll-component";
const SearchPage = ({ data }) => {
const [values, setValues] = useState({
pages: [],
skip: 0,
limit: 5,
hasMore: true,
});
const { pages, skip, limit, hasMore } = values;
const router = useRouter();
const token = getCookie("token");
const getSearchedData = async () => {
console.log(hasMore);
await setValues({ ...values, skip: 0, limit: 5 });
const search = router.query.search;
await searchData(search, skip, limit).then((data) => {
if (data.error) {
console.log(data.error);
} else {
setValues({ ...values, pages: data, skip: 0 });
}
});
};
useEffect(() => {
getSearchedData();
console.log("UseEffect chal gya");
}, [data]);
const mapSearchData = () => {
return pages.map((p, i) => {
return (
<div key={i} className="mt-4">
<Card body className="manual-card">
<CardTitle tag="h5">{p.title}</CardTitle>
<CardText>{p.excerpt}</CardText>
<Button color="primary" className="w-25" size="sm">
<Link href={`/pages/${p.slug}`}>Go To Page</Link>
</Button>
</Card>
</div>
);
});
};
const fetchMoreData = () => {
console.log("Best");
const search = router.query.search;
let toSkip = skip + limit;
searchData(search, toSkip, limit).then((data) => {
if (data.error) {
console.log(data.error);
} else {
const tempArray = [...pages, ...data];
setValues({
...values,
pages: tempArray,
skip: toSkip,
hasMore: !(data.length < limit),
});
}
});
};
return (
<Layout search={router.query.search}>
<Container className="mt-5">
<Col>
{pages && pages.length ? (
mapSearchData()
) : (
<div className="">No Such Pages Present!</div>
)}
{hasMore && (
<Button
className="d-flex mx-auto my-4"
color="primary"
onClick={fetchMoreData}
>
Load More
</Button>
)}
</Col>
;
</Container>
</Layout>
);
};
export default SearchPage;
export async function getServerSideProps(context) {
const { search } = context.query;
// console.log(slug);
if (!search) {
return {
props: { data: null },
};
}
let result = null;
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API}/pages/search/${search}`
);
result = await res.json();
} catch (error) {
return {
props: { data: null },
};
}
return {
props: { data: result },
};
}
When I am on any other page instead of the search page then router.push works fine it resets the skip and limit and fetches all data on starting
But when I am on the search page itself and I search something from there then skip and limit states don't change, data get fetched on basis of skip and limit
And on page reload I Get complete data with skip and limit with the initial state.
In SHort -
My page is having state values skip and limit.
Suppose i have searched Test and i got 15 items, if i have limit of 5, skip of 0
so 15 means my skip will be 10 and limit 5.
Now when i try to search Best and suppose i have only 2 items named best but they are not showing...because on that time, router.push runs on header component and search page reloads but skip value still 10 and not resetting thats my whole problem.
Just reset the value before calling router.push() inside onClick callback of <AiOutlineSearch>
Make these changes in Search Page:
useEffect(() => {
resetState() // create a function to reset the state
getSearchedData();
console.log("UseEffect chal gya");
}, [data, router.query]); // so whenever your search query changes, the state would be reset and new data will be fetched.
May be you should decouple your values state and getSearchData function.
const [skip, setSkip] = useState(0);
const [limit, setLimit] = useState(5);
const [pages, setPages] = useState([]);
// Reset limit and skip states
useEffect(() => {
setLimit(5);
setSkip(0);
}, []); // Try with [react.query.search] if it doesn't work
// Call getSearchedData
useEffect(() => getSearchData(), [limit, skip, react.query.search]);
// Fetch data
const getSearchedData = async () => {
await searchData(search, skip, limit).then((data) => {
if (data.error) {
console.log(data.error);
} else {
setPages(data);
}
});
};
Whenever I dispatch a search action using context and useReducer for an object in an array stored in local storage, it returns the object, but when I delete the search query from the input box, the list is not returned and the page is blank, can anyone help please?
This is my context:
const NotesContext = createContext(null);
const NotesDispatchContext = createContext(null);
const getStoredNotes = (initialNotes = InitialNotes) => {
return JSON.parse(localStorage.getItem("storedNotes")) || initialNotes;
};
export const NotesProvider = ({ children }) => {
const [NOTES, dispatch] = useReducer(NotesReducer, getStoredNotes());
useEffect(() => {
localStorage.setItem("storedNotes", JSON.stringify(NOTES));
}, [NOTES]);
return (
<NotesContext.Provider value={NOTES}>
<NotesDispatchContext.Provider value={dispatch}>
{children}
</NotesDispatchContext.Provider>
</NotesContext.Provider>
);
};
export const useNotesContext = () => {
return useContext(NotesContext);
};
export const useNotesDispatchContext = () => {
return useContext(NotesDispatchContext);
};
const App = () => {
const [query, setQuery] = useState("");
const dispatch = useNotesDispatchContext();
useEffect(() => {
if (query.length !== 0) {
dispatch({
type: "searchNotes",
query: query,
});
}
}, [query]);
return (
<div className="container">
<header>
<Title title={"Notes"} className={"app_title"} />
<form className="search_container">
<span class="material-symbols-outlined">search</span>
<input
type="search"
placeholder="search notes"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</form>
</header>
This is my reducer function
case "searchNotes": {
[...NOTES].filter((note) =>
note.title.toLowerCase().includes(action.query)
);
}
The function seems to actually remove the all data from the local storage instead of filtering based on the query string.
Issue
When you dispatch searchNotes you are changing NOTES and the blow useEffect runs. So if the filter resulted to an empty array, there would be nothing in localStorage.
useEffect(() => {
localStorage.setItem("storedNotes", JSON.stringify(NOTES));
}, [NOTES]);
Solution
What you can do is to remove that useEffect in App that has query as dependency and dispatching searchNotes. And filter directly while rendering, something like this:
{
NOTES.filter((note) => note.title.toLowerCase().includes(query)).map((note, index) => (
<div key={index}>{note.title}</div>
))
}
And at this point you can remove searchNotes case from your reducer.
I'm trying to make react not load until after an axios get requests finishes. I'm pretty rough on react all around, so sorry in advance.
I'm getting an array of objects
const { dogBreedsTest } = useApplicationData()
And I need it to be the default value of one of my states
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest);
However, I'm getting an error that my value is coming up as null on the first iteration of my app starting. How can I ensure that my value has completed my request before my app tries to use it?
Here is how I am getting the data for useApplicationData()
const [dogBreedsTest, setDogBreeds] = useState(null);
const getDogBreeds = async () => {
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
setDogBreeds
}
And I am importing into my app and using:
import useApplicationData from "./hooks/useApplicationData";
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest[0]);
const [breedList1, updateBreedList1] = useState(dogBreedsTest[0])
function handleOnDragEnd(result) {
if (!result.destination) return;
const items = Array.from(dogBreeds);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
for (const [index, item] of items.entries()) {
item['rank'] = index + 1
}
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0])
}
return (
<div className="flex-container">
<div className="App-header">
<h1>Dog Breeds 1</h1>
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="characters">
{(provided) => (
<ul className="dogBreeds" {...provided.droppableProps} ref={provided.innerRef}>
{breedList1?.map(({id, name, rank}, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</div>
)
error: TypeError: Cannot read property 'map' of null
(I am mapping the data later in the program)
const getDogBreeds = async () => {
try {
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds() // -> you are not awaiting this
}, []);
Do this instead
useEffect(() => {
axios.get('https://dog.ceo/api/breeds/list/all')
.then(res => {
const newDogList = generateDogsArray(res.data['message']);
const generatedDogs = selectedDogs(newDogList);
setDogBreeds(generatedDogs);
})
.catch(err => console.log(err));
}, []);
I know this looks awful, but I don't think you should use async/await inside useEffect
Use this in your application
useEffect will update whenever dogBreedsTest is changed. In order to make it work, start with null values and update them to the correct initial values once your async operation is finished.
const { dogBreedsTest } = useApplicationData();
const [dogBreeds, updateDogBreeds] = useState(null);
const [breedList1, updateBreedList1] = useState(null);
useEffect(() => {
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0]);
}, [dogBreedsTest]);
The problem is, that react first render and then run useEffect(), so if you don't want to render nothing before the axios, you need to tell to react, that the first render is null.
Where is your map function, to see the code? to show you it?.
I suppose that your data first is null. So you can use something like.
if(!data) return null
2nd Option:
In your map try this:
{breedList1 === null
? null
: breedList1.map(({id, name, rank}, index) => (
<Draggable
key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable> ))}
You have null, because your axios is async and react try to render before any effect. So if you say to react that the list is null, react will render and load the data from the api in the second time.
Option 1 use the optional chaining operator
dogBreedsTest?.map()
Option 2 check in the return if dogBreedsTest is an array
retrun (<>
{Array.isArray(dogBreedsTest) && dogBreedsTest.map()}
</>)
Option 3 return early
if (!Array.isArray(dogBreedsTest)) return null
retrun (<>
{dogBreedsTest.map()}
</>)
Option 4 set initial state
const [dogBreedsTest, setDogBreeds] = useState([]);
You could also add a loading state and add a loading spinner or something like that:
const [dogBreedsTest, setDogBreeds] = useState(null);
const [loading, setLoading] = useState(true)
const getDogBreeds = async () => {
setLoading(true)
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
setLoading(false)
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
loading,
setDogBreeds
}
Edit
Try to use a useEffect hook to update the states when dogBreedsTest got set.
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest?.[0] ?? []);
const [breedList1, updateBreedList1] = useState(dogBreedsTest?.[0] ?? [])
useEffect(() => {
updateDogBreeds(dogBreedsTest?.[0] ?? [])
updateBreedList1(dogBreedsTest?.[0] ?? [])
}, [dogBreedsTest])
I was trying to set my value in the input value! but after that, I cannot write anything in the input field! I wanted to set values from the back end in value!
We are writing an admin channel to edit the article for that we need already existing article values to edit the article! What am I doing wrong! or Maybe you can suggest a better way to edit the article in the admin channel!
here is the code:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams } from 'react-router';
const EditArticle = (props) => {
const [editValues, setEditValues] = useState([]);
const [changedValues, setChangedValues] = useState('');
console.log('values', editValues);
console.log('changed', changedValues);
const params = useParams();
console.log(params);
const resultsId = params.id;
console.log('string', resultsId);
const [authTokens, setAuthTokens] = useState(
localStorage.getItem('token') || ''
);
const setTokens = (data) => {
localStorage.setItem('token', JSON.stringify(data));
setAuthTokens(data);
// setToken(data['dataValues']['token']);
};
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get(
`${process.env.REACT_APP_API_URL}/article/${resultsId}`
);
setEditValues(res.data);
} catch (err) {}
};
fetchData();
}, [resultsId]);
const inputValue = editValues;
const userToken = props.token;
return (
<div>
<form value={{ authTokens, setAuthTokens: setTokens }}>
<input
value={editValues.title || ''}
onChange={(input) => setChangedValues(input.target.value)}
type='text'
/>
<input
// ref={editValues.shortDesc}
value={editValues.shortDesc}
onChange={(input) => setChangedValues(input.target.value)}
type='text'
/>
<button type='submit'>send</button>
</form>
</div>
);
};
export default EditArticle;
your onChange handler is updating a different state property than what is being used as the value on the input (editValues vs changedValues).
Also you can pass a defaultValue to input that will get used as the default value only.
See more here https://reactjs.org/docs/uncontrolled-components.html
you can use just do it just using editValues. try this:
I just reproduced it without the api call to run the code.
import React, { useState, useEffect } from "react";
const EditArticle = (props) => {
const [editValues, setEditValues] = useState([]);
console.log("values", editValues);
const [authTokens, setAuthTokens] = useState(
localStorage.getItem("token") || ""
);
const setTokens = (data) => {
localStorage.setItem("token", JSON.stringify(data));
setAuthTokens(data);
// setToken(data['dataValues']['token']);
};
useEffect(() => {
const fetchData = async () => {
try {
//here get the data from api and setstate
setEditValues({ title: "title", shortDesc: "shortDesc" });
} catch (err) {}
};
fetchData();
}, []);
return (
<div>
<form value={{ authTokens, setAuthTokens: setTokens }}>
<input
value={editValues.title || ""}
onChange={(input) => setEditValues({title: input.target.value})}
type="text"
/>
<input
value={editValues.shortDesc}
onChange={(input) => setEditValues({shortDesc: input.target.value})}
type="text"
/>
<button type="submit">send</button>
</form>
</div>
);
};
export default EditArticle;
I have two React components, namely, Form and SimpleCheckbox.
SimpleCheckbox uses some of the Material UI components but I believe they are irrelevant to my question.
In the Form, useEffect calls api.getCategoryNames() which resolves to an array of categories, e.g, ['Information', 'Investigation', 'Transaction', 'Pain'].
My goal is to access checkboxes' states(checked or not) in the parent component(Form). I have taken the approach suggested in this question.(See the verified answer)
Interestingly, when I log the checks it gives(after api call resolves):
{Pain: false}
What I expect is:
{
Information: false,
Investigation: false,
Transaction: false,
Pain: false,
}
Further More, checks state updates correctly when I click into checkboxes. For example, let's say I have checked Information and Investigation boxes, check becomes the following:
{
Pain: false,
Information: true,
Investigation: true,
}
Here is the components:
const Form = () => {
const [checks, setChecks] = useState({});
const [categories, setCategories] = useState([]);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
useEffect(() => {
api
.getCategoryNames()
.then((_categories) => {
setCategories(_categories);
})
.catch((error) => {
console.log(error);
});
}, []);
return (
{categories.map(category => {
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
/>
}
)
}
const SimpleCheckbox = ({ onCheck, label, id }) => {
const [check, setCheck] = useState(false);
const handleChange = (event) => {
setCheck(event.target.checked);
};
useEffect(() => {
onCheck(check, id);
}, [check]);
return (
<FormControl>
<FormControlLabel
control={
<Checkbox checked={check} onChange={handleChange} color="primary" />
}
label={label}
/>
</FormControl>
);
}
What I was missing was using functional updates in setChecks. Hooks API Reference says that: If the new state is computed using the previous state, you can pass a function to setState.
So after changing:
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
to
const handleCheckChange = (isChecked, category) => {
setChecks(prevChecks => { ...prevChecks, [category]: isChecked });
}
It has started to work as I expected.
It looks like you're controlling state twice, at the form level and at the checkbox component level.
I eliminated one of those states and change handlers. In addition, I set checks to have an initialState so that you don't get an uncontrolled to controlled input warning
import React, { useState, useEffect } from "react";
import { FormControl, FormControlLabel, Checkbox } from "#material-ui/core";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form />
</div>
);
}
const Form = () => {
const [checks, setChecks] = useState({
Information: false,
Investigation: false,
Transaction: false,
Pain: false
});
const [categories, setCategories] = useState([]);
console.log("checks", checks);
console.log("categories", categories);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
};
useEffect(() => {
// api
// .getCategoryNames()
// .then(_categories => {
// setCategories(_categories);
// })
// .catch(error => {
// console.log(error);
// });
setCategories(["Information", "Investigation", "Transaction", "Pain"]);
}, []);
return (
<>
{categories.map(category => (
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
check={checks[category]}
/>
))}
</>
);
};
const SimpleCheckbox = ({ onCheck, label, check }) => {
return (
<FormControl>
<FormControlLabel
control={
<Checkbox
checked={check}
onChange={() => onCheck(!check, label)}
color="primary"
/>
}
label={label}
/>
</FormControl>
);
};
If you expect checks to by dynamically served by an api you can write a fetchHandler that awaits the results of the api and updates both slices of state
const fetchChecks = async () => {
let categoriesFromAPI = ["Information", "Investigation", "Transaction", "Pain"] // api result needs await
setCategories(categoriesFromAPI);
let initialChecks = categoriesFromAPI.reduce((acc, cur) => {
acc[cur] = false
return acc
}, {})
setChecks(initialChecks)
}
useEffect(() => {
fetchChecks()
}, []);
I hardcoded the categoriesFromApi variable, make sure you add await in front of your api call statement.
let categoriesFromApi = await axios.get(url)
Lastly, set your initial slice of state to an empty object
const [checks, setChecks] = useState({});