I have a react native screen that has a very long code that I would like to refractor.
Say my screen.jsx is (simplified, of course):
import React, { useState, useCallback, useEffect } from 'react';
import useLocation from '../hooks/useLocation'; // A custom hook I wrote. This one makes sense to use as a hook. It's a function that returns a location.
...
export default function Screen() {
const [fetchingLocation, region, setRegion] = useLocation();
// FROM HERE DOWN
const [fetchingRestaurants, setFetchingRestaurants] = useState(false);
const [restaurants, setRestaurants] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
const initSearch = useCallback(async ({ searchQuery, region }) => {
setFetchingRestaurants(true);
try {
const response = await remoteApi.get('/search', {
params: {
term: searchQuery,
latitude: region.latitude,
longitude: region.longitude,
},
});
const fetchedRestaurants = response.data.businesses;
const fetchedRestaurantsArray = fetchedRestaurants.map((restaurant) => ({
id: restaurant.id,
name: restaurant.name,
}));
setRestaurants(fetchedRestaurantsArray);
setFetchingRestaurants(false);
} catch (e) {
setRestaurants([]);
setFetchingRestaurants(false);
}
}, []);
return (
<View>...</View>
);
}
To better structure my code, I would like to move all the code you see below "FROM HERE DOWN" (initSearch as well as the three state management useState hooks above it) into another file and import it.
At the moment I created a custom useRestaurantSearch hook in the hooks folder like so:
export default function useRestaurantSearch() {
// The code I mentioned goes here
return [initSearch, errorMessage, restaurants, setRestaurants, fetchingRestaurants];
}
Then in my Screen.jsx file I import it import useRestaurantSearch from '../hooks/useRestaurantSearch'; and inside function Screen() I grab the consts I need with
const [
initSearch,
errorMessage,
restaurants,
setRestaurants,
fetchingRestaurants,
] = useRestaurantSearch();
This works, but I feel like it can be better written and this whole approach seems weird - is it really a custom hook? If it's not a custom hook, does it belong in a util folder as a utility?
How would you approach this?
Yes this would be considered a custom hook since according to the React docs, custom hooks are just a mechanism to reuse stateful logic.
One thing that could help simplify it is:
using a library like TanStack Query (formerly React Query). You could create a query to fetch the restaurants and then you could use the data and fetchStatus from the query instead of adding them to state.
Related
In my component I use a function which I want to extract, it uses some hooks for setting url params. I created a custom hook.
function useMyCustomHook() {
const history = useHistory();
const location = useLocation();
const locationParams = new URLSearchParams(location.search);
function myCustomHook(id: string ) {
does something
}
return myCustomHook;
}
So I extracted it like shown above and now I want to use it in my other component and inside a useEffect hook.
const { myCustomHook } = useMyCustomHook(); // It asks me to add parameters,
but I want to pass it inside useEffect
useEffect(() => {
if (something) myCustomHook(myParam);
}, [foo]);
Is this a possible approach? Or is there a better solution where I can extract something with hooks and then reuse it in useEffect with parameters? Thank you!
First you need export your custom Hook, I think if you need return a function with id, that function need be executed each id change.
custom hook
import { useCallback} from "react"
import {useNavigate, useLocation} from "react-router-dom"
export const useMyCustomHook = (id) => {
const navigate = useNavigate() // React-router v6
const location = useLocation()
const locationParams = new URLSearchParams(location.search)
const anyFunction = useCallback(() => {
// does something
}, [id]) // the function execute if "id" change
return anyFunction
}
where you wanna use your custom hook
import {useMyCustomHook} from "your router"
const {anyFunction} = useMyCustomHook(id) // your pass the id
useEffect(() => {
if (something) anyFunction()
}, [foo])
I think this is the better way. useCallback only render the function of the params change.
I'm making a Quiz app with react and typescript.
I have created a quiz context provider to wrap the functionality and pass it to the children.
The value inside my quiz provider is presented with a custom hook called useQuiz that handles all of my game logic - receives (category from url-params, questions from the backend) and returns useful data and methods to play it effectively.
Unfortunately, because of my pre-made custom hook, I can't wait for the questions data to fetch and render {children} as a result. When I'll add conditions to my jsx (for instance, display standby screen while waiting), react rules of hooks will be broken.
However, if I would write useQuiz logic inside the provider, it will fix my problem. but the structure might be messy for reading.
In my code example, react first render the page with questions marked as undefined. To overcome the error, I have added questionsJson file to be the default questions before fetching (just for demonstration purposes).
I'd like some help to still use useQuiz in my context provider and render a loading page, without breaking react rules. Alternatively, I would be glad to hear other suggestions or patterns.
Down below I added the code referenced to my explanation.
Any help will be appreciated :)
QuizProvider :
import { createContext, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { Question, Provider } from '../types';
import useQuiz from '../hooks/useQuiz';
import questionsJson from '../lib/questions.json';
import useFetch from '../hooks/useFetch';
export const QuizContext = createContext({} as ReturnType<typeof useQuiz>);
export default function _QuizProvider({ children }: Provider) {
const { pathname } = useLocation();
const { category } = useParams();
const { fetchData: fetchQuestions, data: questions, loading, error } = useFetch<Question[]>();
useEffect(() => {
fetchQuestions(`${pathname}/questions`, 'GET');
} , []);
return (
<QuizContext.Provider value={useQuiz({ category: category!, questions: questions || questionsJson.geography })}>
{children}
</QuizContext.Provider>
);
}
EDIT
useQuiz :
import { useState } from 'react';
import { Answer, Quiz, useQuiz } from '../types';
export default function _useQuiz({ category, questions }: useQuiz): Quiz {
const QUESTIONS_TIMER = 10 * 60;
const [questionNo, setQuestionNo] = useState(1);
const [answers, setAnswers] = useState(new Array<Answer>(questions.length));
const [finish, setFinish] = useState(false);
return {
category,
questions,
timer: QUESTIONS_TIMER,
questionNo,
totalQuestions: questions.length,
currentQuestion: questions[questionNo - 1],
finish,
nextQuestion: () => setQuestionNo(questionNo + 1),
previousQuestion: () => setQuestionNo(questionNo - 1),
toggleQuestion: (index: number) => setQuestionNo(index),
onAnswer: (answer: Answer) => {
const newAnswers = [...answers];
newAnswers[questionNo - 1] = answer;
setAnswers(newAnswers);
},
isSelected: (answer: Answer) =>
JSON.stringify(answers[questionNo - 1]) === JSON.stringify(answer),
isAnswersMarked: () =>
answers.every(answer => answer !== undefined),
finishQuiz: () => setFinish(true),
score: () =>
answers.filter(answer => answer && answer.correct).length
};
}
I realize that my structure is illegal.
I tried to render a custom hook after several condition which breaks react rules.
So, there are two pattern that could solve my issue:
Replacing the custom hook with a component.
Replacing the custom hook with a reducer function. That’s a perfect fit for me.
I hope it’ll help you :)
I am trying to learn to work with custom Hooks in React-native. I am using AWS Amplify as my backend, and it has a method to get the authenticated user's information, namely the Auth.currentUserInfo method. However, what it returns is an object and I want to make a custom Hook to both returns the part of the object that I need, and also abstract away this part of my code from the visualization part. I have a component called App, and a custom Hook called useUserId. The code for them is as follows:
The useUserId Hook:
import React, { useState, useEffect } from "react";
import { Auth } from "aws-amplify";
const getUserInfo = async () => {
try {
const userInfo = await Auth.currentUserInfo();
const userId = userInfo?.attributes?.sub;
return userId;
} catch (e) {
console.log("Failed to get the AuthUserId", e);
}
};
const useUserId = () => {
const [id, setId] = useState("");
const userId = getUserInfo();
useEffect(() => {
userId.then((userId) => {
setId(userId);
});
}, [userId]);
return id;
};
export default useUserId;
The App component:
import React from "react";
import useUserId from "../custom-hooks/UseUserId";
const App = () => {
const authUserId = useUserId();
console.log(authUserId);
However, when I try to run the App component, I get the same Id written on the screen twice, meaning that the App component is executed again.
The problem with this is that I am using this custom Hook in another custom Hook, let's call it useFetchData that fetches some data based on the userId, then each time this is executed that is also re-executed, which causes some problems.
I am kind of new to React, would you please guide me on what I am doing wrong here, and what is the solution to this problem. Thank you.
The issue is likely due to the fact that you've declared userId in the hook body. When useUserId is called in the App component it declares userId and updates state. This triggers a rerender and userId is declared again, and updates the state again, this time with the same value. The useState hook being updated to the same value a second time quits the loop.
Bailing out of a state update
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects.
(React uses the Object.is comparison algorithm.)
Either move const userId = getUserInfo(); out of the useUserId hook
const userId = getUserInfo();
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
userId.then((userId) => {
setId(userId);
});
}, []);
return id;
};
or more it into the useEffect callback body.
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
getUserInfo().then((userId) => {
setId(userId);
});
}, []);
return id;
};
and in both cases remove userId as a dependency of the useEffect hook.
Replace userId.then with to getUserId().then. It doesn't make sense to have the result of getUserId in the body of a component, since it's a promise and that code will be run every time the component renders.
I have some React Redux code written in Typescript that loads some data from my server when a component mounts. That code looks like this:
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { MyAction } from 'my/types/MyAction';
export const useDispatchOnMount = (action: MyAction) => {
const dispatch = useDispatch();
return useEffect(() => {
dispatch(action);
}, [dispatch]);
};
This is simple enough - it uses the useEffect hook to do what I want. Now I need to convert the code so that it uses MobX instead of Redux for persistent state. If I have my own MobX store object called myStore, and myStore has a async method "loadXYZ" that loads a specific set of data XYZ, I know I can do this inside my component:
useEffect(() => {
async functon doLoadXYZ() {
await myStore.loadXYZ();
}
doLoadXYZ();
}, []);
This does indeed work, but I would like to put all this into a single fat arrow function that calls useEffect, much like what the useDispatchOnMount function does. I can't figure out the best way to do this. Anyone know how to do this?
EDIT: After further digging, it looks more and more like what I am trying to do with the Mobx version would break the rules of Hooks, ie always call useEffect from the top level of the functional component. So calling it explicitly like this:
export const MyContainer: React.FC = () => {
useEffect(() => {
async functon doLoadXYZ() {
await myStore.loadXYZ();
}
doLoadXYZ();
}, []);
...
};
is apparently the best way to go. Butthat raises the question: is the redux version that uses useDispatchOnMount a bad idea? Why?
You can do this if you don't use async/await in the useEffect. If you are fetching data, I would store it in myStore and use it directly out of the store instead of using async/await. It might look something like this:
export const SomeComp = observer(function SomeComp() {
const myStore = useStore() // get the store with a hook or how you are getting it
useEffect(myStore.loadXYZ, [myStore])
return <div>{myStore.theLoadedData}</div>
})
In loadXYZ you just store the data the way you want and use it. The component observing theLoadedData will re-render when it comes back so you don't need to have async/await in the component.
I have a React Component like shown bellow (some parts are ommited) and I'm using redux for state management. The getRecentSheets action contains an AJAX request and dispatches the response to redux which updates state.sheets.recentSheets with the response's data.
All this works as expected, but on building it throws warning about useEffect has a missing dependency: 'getRecentSheets'. But if I add getRecentSheets to useEffect's dependency array it starts to rerun indefinitely and thus freezes the app.
I've read React documentation about the useEffect hook https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies but it doesn't provide a good example for such usecase. I suppose it is something with useCallback or react-redux useDispatch, but without examples I'm not sure how to implement it.
Can someone please tell me what the most concise and idiomatic way to use redux action in useEffect would be and how to avoid warnings about missing dependencies?
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import SheetList from '../components/sheets/SheetList';
import { getRecentSheets } from '../store/actions/sheetsActions';
const mapStateToProps = (state) => {
return {
recentSheets: state.sheets.recentSheets,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getRecentSheets: () => dispatch(getRecentSheets()),
}
}
const Home = (props) => {
const {recentSheets, getRecentSheets} = props;
useEffect(() => {
getRecentSheets();
}, [])
return <SheetList sheets={ recentSheets } />
};
export default connect(mapStateToProps, mapDispatchToProps) (Home);
After all, it seems that correct way will be as follows:
// ...
import { useDispatch } from 'react-redux';
import { getRecentSheets } from '../store/actions/sheetsActions';
const Home = props => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getRecentSheets());
}, [dispatch])
// ...
};
This way it doesn't complain about getRecentSheets missing in dependencies array. As I understood from reading React doc on hooks that's because it's not defined inside the component. Though I'm new to frontend and I hope I didn't mess something up here.
Passing an empty array in your hook tells React your hook function will not have any dependent values from either props or state.
useEffect(() => {
getRecentSheets();
}, [])
The infinite loop arises when you declare the dispatcher as a dependency on the hook. When the component is initialized, props.recentSheets hasn't been set, and will rerender once you make your AJAX call.
useEffect(() => {
getRecentSheets();
}, [getRecentSheets])
You could try something like this:
const Home = ({recentSheets}) => {
const getRecentSheetsCallback = useCallback(() => {
getRecentSheets();
})
useEffect(() => {
getRecentSheetsCallback();
}, [recentSheets]) // We only run this effect again if recentSheets changes
return <SheetList sheets={ recentSheets } />
};
No matter how many times Homes re-renders, you retain the memoized function to your dispatch call.
Alternatively, you may have encountered find similar patterns utilizing local state and then make your effect "depend" on sheets.
const [sheets, setSheets] = useState(recentSheets)
Hope this helps
I would add a check to see if recentSheets exists or not, using that as my dependency.
useEffect(() => {
if (!recentSheets) getRecentSheets();
}, [recentSheets])