What I am trying to achieve:
Retrieve book-> take book.chapterIds[0] to update currentChapter -> take currentChapter to update chapters
I am using one state variable(Book) to set another state variable(chapters), like so:
useEffect(() => {
getBook(match.params.id);
// eslint-disable-next-line
}, []);
useEffect(() => {
setCurrentChapter(book.chapterIds[0]);
// eslint-disable-next-line
}, [book]);
useEffect(() => {
getChapter(currentChapter);
// eslint-disable-next-line
}, [currentChapter]);
For second useEffect, I end up getting: Uncaught TypeError: book.chapterIds is undefined
Here is what I tried:
useEffect(() => {
if (Object.keys(book).length !== 0) {
setCurrentChapter(book.chapterIds[0]);
}
// eslint-disable-next-line
}, [book]);
which kinda works, but I still ends up triggering:
useEffect(() => {
getChapter(currentChapter);
// eslint-disable-next-line
}, [currentChapter]);
where both book and currentChapter is undefined
App.js
const [book, setBook] = useState({});
const [chapters, setChapters] = useState({});
const [currentChapter, setCurrentChapter] = useState();
const [loading, setLoading] = useState(false);
const getBook = async (id) => {
setLoading(true);
const res = await axios.get(`<someurl><with id>`);
console.log(res.data);
setBook(res.data.book);
setLoading(false);
};
const getChapter = async (chapterId) => {
if (chapters[chapterId] === undefined) {
console.log(`<someurl><with id & chapterId>`);
setLoading(true);
const res = await axios.get(
`<someurl><with id & chapterId>`
);
setLoading(false);
console.log(res.data);
setChapters({
...chapters,
[chapterId]: res.data.chapter,
});
}
};
Book.js
useEffect(() => {
getBook(match.params.id);
// eslint-disable-next-line
}, []);
useEffect(() => {
if (Object.keys(book).length !== 0) {
setCurrentChapter(book.chapterIds[0]);
}
// eslint-disable-next-line
}, [book]);
useEffect(() => {
getChapter(currentChapter);
// eslint-disable-next-line
}, [currentChapter]);
Also, I get book.chapterIds as undefined on using it inside Book componentreturn()
What am I doing wrong?
Try to set all initial states as null:
const [book, setBook] = useState(null);
const [chapters, setChapters] = useState(null);
const [currentChapter, setCurrentChapter] = useState(null);
Then your useEffects:
useEffect(() => {
getBook(match.params.id);
// eslint-disable-next-line
}, []);
useEffect(() => {
if(book && book.chapterIds?.length > 0)
setCurrentChapter(book.chapterIds[0]);
// eslint-disable-next-line
}, [book]);
useEffect(() => {
if(currentChapter)
getChapter(currentChapter);
// eslint-disable-next-line
}, [currentChapter]);
Related
I'm trying to use a useFetch custom hook on a small todolist app that I'm working on to learn React.
I don't get why my useFetch function seems to work but its inner useEffect never triggers.
I tried removing the URL from dependencies array, adding the URL as an argument of the useEffect but nothing happened: my variable [response] stays null.
Here is the code for the useFetch :
utils.js:
export function useFetch(url) {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
console.log(url);
if (url === undefined) return;
const fetchData = async () => {
setIsLoading(true);
try {
const result = await getRequest(url);
setResponse(result);
setIsLoading(false);
} catch (error) {
setError(error);
}
};
fetchData();
}, [url]);
return [response, setResponse, error, isLoading];
}
App.js:
import { useState, useMemo, useCallback } from 'react';
import { useFetch, postRequest, deleteRequest, getFormatedDate } from './utils';
//more imports
export default function App() {
const [response] = useFetch('/items');
const [titleValue, setTitleValue] = useState('');
const [descriptionValue, setDescriptionValue] = useState('');
const [deadlineValue, setDeadlineValue] = useState(new Date());
const [doneFilter, setDoneFilter] = useState(0);
const [selectedItem, setSelectedItem] = useState();
const [showDialog, setShowDialog] = useState(false);
const onSave = useCallback(
async () => {
if (titleValue) {
let valueToSave = {};
valueToSave.title = titleValue;
valueToSave.status = false;
if (descriptionValue) valueToSave.description = descriptionValue;
valueToSave.deadline = deadlineValue instanceof Date ? deadlineValue : new Date();
setData((prev) => [...prev, valueToSave]);
setTitleValue('');
setDescriptionValue('');
setDeadlineValue(new Date());
try {
await postRequest('add', valueToSave);
} catch (err) {
console.error(err);
throw err;
}
}
},
[descriptionValue, titleValue, deadlineValue]
);
const onDelete = useCallback(async (item) => {
setData((items) => items.filter((i) => i !== item));
try {
await deleteRequest(item._id);
} catch (err) {
console.error(err);
throw err;
}
}, []);
const onModif = useCallback(async (id, field) => {
const res = await postRequest('update/' + id, field);
if (res.ok) setShowDialog(false);
}, []);
const organizedData = useMemo(() => {
if (!response) return;
for (let d of response) d.formatedDeadline = getFormatedDate(d.deadline);
response.sort((a, b) => new Date(a.deadline) - new Date(b.deadline));
if (doneFilter === 1) return response.filter((e) => e.status);
else if (doneFilter === 2) return response.filter((e) => !e.status);
else return response;
}, [response, doneFilter]);
//more code
return (
// jsx
)}
console.logging works just above the useEffect but never inside.
I cannot easily recreate your issue but I can point out some issues with your useFetch hook -
function useFetch(url) {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
console.log(url);
if (url === undefined) return;
const fetchData = async () => {
setIsLoading(true);
try {
const result = await getRequest(url);
setResponse(result);
setIsLoading(false);
} catch (error) {
setError(error);
// ❌ loading == true
}
};
fetchData();
// ❌ what about effect cleanup?
}, [url]);
return [response, setResponse, error, isLoading]; // ❌ don't expose setResponse
}
Check out Fetching Data from the react docs. Here's the fixes -
function useFetch(url) {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(
() => {
if (url == null) return;
let mounted = true // ✅ component is mounted
const fetchData = async () => {
try {
if (mounted) setIsLoading(true); // ✅ setState only if mounted
const response = await getRequest(url);
if (mounted) setResponse(response); // ✅ setState only if mounted
} catch (error) {
if (mounted) setError(error); // ✅ setState only if mounted
} finally {
if (mounted) setIsLoading(false); // ✅ setState only if mounted
}
};
fetchData();
return () => {
mounted = false // ✅ component unmounted
}
},
[url]
);
return { response, error, isLoading }
}
When you use it, you must check for isLoading first, then null-check the error. If neither, response is valid -
function MyComponent() {
const {response, error, isLoading} = useFetch("...")
if (isLoading) return <Loading />
if (error) return <Error error={error} />
return (
// response is valid here
)
}
See this Q&A for a more useful useAsync hook.
When I load my Nextjs page, I get this error message: "Error: Rendered more hooks than during the previous render."
If I add that if (!router.isReady) return null after the useEffect code, the page does not have access to the solutionId on the initial load, causing an error for the useDocument hook, which requires the solutionId to fetch the document from the database.
Therefore, this thread does not address my issue.
Anyone, please help me with this issue!
My code:
const SolutionEditForm = () => {
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
if (!router.isReady) return null
const { document } = useDocument("solutions", solutionId)
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
return (
<div>
// JSX code
</div>
)
}
useDocument hook:
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id])
return { document, isLoading, error }
}
You cannot call a hook, useEffect, your custom useDocument, or any other after a condition. The condition in your case is this early return if (!router.isReady) returns null. As you can read on Rules of Hooks:
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns...
Just remove that if (!router.isReady) returns null from SolutionEditForm and change useDocument as below.
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!id) return; // if there is no id, do nothing 👈🏽
const ref = doc(db, c, id);
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false);
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id });
setError(null);
} else {
setError("No such document exists");
}
},
(err) => {
console.log(err.message);
setIsLoading(false);
setError("failed to get document");
}
);
return () => unsubscribe();
}, [c, id]);
return { document, isLoading, error };
};
The if (!router.isReady) return null statement caused the function to end early, and subsequent hooks are not executed.
You need to restructure your hooks such that none of them are conditional:
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
const { document } = useDocument("solutions", solutionId, router.isReady) // pass a flag to disable until ready
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
// Move this to after the hooks.
if (!router.isReady) return null
and then to make useDocument avoid sending extra calls:
export const useDocument = (c, id, enabled) => {
and updated the effect with a check:
useEffect(() => {
if (!enabled) return;
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id, enabled])
UseEffect cannot be called conditionally
UseEffect is called only on the client side.
If you make minimal representation, possible to try fix this error
I'm developing a cart system and the problem is that, when I add a product to the cart, it works in context and localStorage; but, when I refresh, the data is gone.
const dispatch = useDispatch();
const {
cartItems
} = useSelector((state) => state.cart)
const [cartState, setCartState] = useState({
cartItems: [],
})
const initialRender = useRef(true);
useEffect(() => {
if (JSON.parse(localStorage.getItem("cartState"))) {
const storedCartItems = JSON.parse(localStorage.getItem("cartState"));
setCartState([...cartItems, ...storedCartItems]);
}
}, []);
useEffect(() => {
if (initialRender.current) {
initialRender.current = false;
return;
}
window.localStorage.setItem("cartState", JSON.stringify(cartState));
}, [cartState]);
What I usually do is have some state to check against to see if the client side is loaded:
const [clientLoaded, setClientLoaded] = useState(false);
useEffect(() => {
setClientLoaded(true);
}, []);
Then anywhere you can check if clientLoaded is true, for example in another useEffect hook:
useEffect(() => {
clientLoaded && doWhatever; // do whatever you want here
}, [clientLoaded]);
Or in you render method:
{clientLoaded && <span>Render this if the client is loaded</span>}
You are earsing the value of the local storage on the first render
useEffect(() => {
if (initialRender.current) {
initialRender.current = false;
return;
}
//here
window.localStorage.setItem("cartState", JSON.stringify(cartState));
}, [cartState]);
You should :
useEffect(() => {
if (initialRender.current) {
initialRender.current = false;
} else {
window.localStorage.setItem("cartState", JSON.stringify(cartState));
}
}, [cartState]);
I am trying to save the user data when he loged in like this.
const handleLogin = () => {
firebase
.auth()
.signInWithEmailAndPassword(Email, passWord)
.then((res) => {
firebase.auth().onAuthStateChanged((userData) => {
setuserData(userData);
const jsonValue = JSON.stringify(userData);
AsyncStorage.setItem("userData", jsonValue);
console.log(userData);
});
})
.then(() => navigation.navigate("HomeScreen"))
.catch((error) => console.log(error));
};
and in the Spalch I am trying to check if the userData is in local storage or not .the problem is that it goes directly to HomeScreen even if there is No Data in Local storage
any help please
const SplashScreen = ({ navigation }) => {
const [animating, setAnimating] = useState(true);
useEffect(() => {
setTimeout(() => {
setAnimating(true);
navigation.replace(AsyncStorage.getItem("userData") ? "HomeScreen" : "Log_In");
}, 500);
},
[]);
AsyncStorage.getItem returns promise so either you need to write it with async/await or in promise
useEffect(() => {
setTimeout(async() => {
setAnimating(true);
const user = await AsyncStorage.getItem("userData")
navigation.replace(user ? "HomeScreen" : "Log_In");
}, 500);
},
[]);
Here is my solution for this, a bit long but you can try it out
const SplashScreen = ({ navigation }) => {
const [animating, setAnimating] = useState();
const [redirect, setRedirect] = useState('');
const getUserData = useCallback(async () => {
const response = await AsyncStorage.getItem('userData');
setRedirect(response ? 'HomeScreen' : 'Log_In');
},[]);
useEffect(() => {
setTimeout(() => {
getUserData();
}, 500);
}, []);
useEffect(() => {
if (redirect) {
setAnimating(true);
navigation.replace(redirect);
}
}, [redirect]);
};
I have created this custom hook to fetch data:
const useSuggestionsApi = () => {
const [data, setData] = useState({ suggestions: [] });
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const fetchData = () => {
setError(false);
setLoading(true);
if(url) {
fetch(url).then((res) => {
if (res.status !== 200) {
console.error(`It seems there was an problem fetching the result. Status Code: ${res.status}`)
return;
}
res.json().then((fetchedData) => {
setData(fetchedData)
})
}).catch(() => {
setError(true)
})
setLoading(false);
};
}
fetchData();
}, [url]);
return [{ data, loading, error }, setUrl];
}
export default useSuggestionsApi;
It used used in this component to render the response (suggestions).
const SearchSuggestions = ({ query, setQuery}) => {
const [{ data }, doFetch] = useSuggestionsApi();
const { suggestions } = data;
useEffect(() => {
const encodedURI = encodeURI(`http://localhost:3000/search?q=${query}`);
doFetch(encodedURI);
}, [doFetch, query]);
return (
<div className="search-suggestions__container">
<ul className="search-suggestions__list">
{suggestions.map((suggestion) => {
return (
<li className="search-suggestions__list-item" key={uuid()}>
<span>
{suggestion.searchterm}
</span>
</li>
)
})}
</ul>
</div>
);
};
export default SearchSuggestions;
Now I would like to write some unit test for the SearchSuggestions component but I am lost on how to mock the returned data from useSuggestionApi. I tried importing useSuggestionApi as a module and then mocking the response like this but with no success:
describe('SearchSuggestions', () => {
const wrapper = shallow(<SearchSuggestions/>)
it('test if correct amount of list-item elements are rendered', () => {
jest.mock("../hooks/useSuggestionsApi", () => ({
useSuggestionsApi: () => mockResponse
}));
expect(wrapper.find('.search-suggestions__list').children()).toHaveLength(mockResponse.data.suggestions.length);
});
})
I am new to testing React components so very grateful for any input!
This works:
jest.mock('../hooks/useSuggestionsApi', () => {
return jest.fn(() => [{data: mockResponse}, jest.fn()]
)
})
describe('SearchSuggestions', () => {
const wrapper = shallow(<SearchSuggestions query="jas"/>)
it('correct amount of list-items gets rendered according to fetched data', () => {
expect(wrapper.find('.search-suggestions__list').children()).toHaveLength(mockResponse.suggestions.length);
});
})