Fetching w/ custom React hook based on router parameters - reactjs

I'm trying to fetch data with a custom React hook, based on the current router parameters.
https://stackblitz.com/edit/react-fetch-router
What it should do:
On first load, check if URL contains an ID...
If it does, fetch a todo with that ID
If it does not, fetch a todo with a random ID & add ID to url
On fetch button clicks...
Fetch a todo with a random ID & add ID to url
What is wrong:
Watching the console or inspector network tab, you can see that it's firing several fetch requests on each click - why is this and how should this be done correctly?

Since you used history.push on handleClick, you will see multiple requests being sent as you are using history.push on click handler which will cause a re-render and make use use random generated url as well as also trigger the fetchTodo function.
Now a re-render will occur which is cause a randomised id to be generated. and passed onto useTodo hook which will lead to the fetchTodo function being called again.
The correct solution for you is to set the random todo id param on handleClick and also avoid unnecessary url updates
const Todos = () => {
const history = useHistory();
const { id: todoId } = useParams();
const { fetchTodo, todo, isFetching, error } = useTodo(todoId);
const isInitialRender = useRef(true);
const handleClick = () => {
const todoId = randomMax(100);
history.push(`/${todoId}`);
};
useEffect(() => {
history.push(`/${todoId}`);
}, []);
useEffect(() => {
if(!isInitialRender.current) {
fetchTodo();
} else {
isInitialRender.current = false
}
}, [todoId])
return (
<>
<button onClick={handleClick} style={{marginBottom: "10px"}}>Fetch a todo</button>
{isFetching ? (
<p>Fetching...</p>
) : (
<Todo todo={todo} color={todo.color} isFetching={isFetching} />
)
}
</>
);
};
export default Todos;
Working demo

Related

React useSelector and getState returning different values in useEffect

I've been following the official Redux tutorial to create and dispatch asynchronous thunks (created with createAsyncThunk) to load some user state.
I have two components, say A and B, and they both need access to the same userState information. I'm not sure which will complete rendering first, so I've added the ability to dispatch the information they need to both:
// both in component A and B ...
const status = useSelector(selectUserStateStatus);
useEffect(()=>{
if (status === "idle") {
dispatch(fetchUserState());
}
}, [status] );
//...
However, both are consistently dispatching fetchUserState() so I wind up with unnecessary duplicate requests.
I would think that (in the scenario that the useEffect triggers in A first):
A completes render, useEffect is called when status==="idle"
fetchUserState is dispatched, setting status = "loading"
A re-render of B is triggered, with status === "loading"
B doesn't send a new request
However, this is not what happens. A and B both dispatch fetchUserState, resulting in duplicate dispatches.
If I use store.getState() however, expected behavior results with only a single dispatch. IE:
useEffect(()=>{
if (store.getState().status === "idle") {
dispatch(fetchUserState());
}
}, [store.getState().status] );
This feels like an anti-pattern though. I'm confused by this behavior, any suggestions on how to remedy this or archetype it more effectively?
use lodash/throttle on the API layer for cancelling first request
use common handler for all nesting components
function UserStateProvider() {
const isInitRef = useRef(false)
const status = useSelector(selectUserStateStatus);
const fetchUser = () => {
if (isInitRef.current === false) {
dispatch(fetchUserState())
isInitRef.current = true
}
}
return <ContextProvider value={{
status: status, // if you need status
fetchUser: fetchUser,
}}>
{children}
</ContextProvider>
}
const A = () => {
const {fetchUser} = useUserStateContext()
useEffect(() => {
fetchUser()
}, [])
}
<UserStateProvider>
<A />
<B />
<A />
</UserStateProvider>
// sugar, if all what you want it is trigger loading user
const useLoadUser = () => {
const {fetchUser} = useUserStateContext()
useEffect(() => {
fetchUser()
}, [])
}
const A = () => {
useLoadUser()
}

How to make setting state with useEffect() to run on page refresh?

My code is not long or complicated at all. It's simple. so please read!
(Im using react + next.js)
In the root file, app.js, I have useEffect to fetch photo data. This data array will be used in a page component so I pass it down from app.js via <Component.../>
function MyApp({ Component, pageProps }) {
const [photoData, setPhotoData] = useState([]);
const [user, setUser] = useState([]);
useEffect(() => {
const getPhotos = async () => {
try {
const photoData = await axios.get(
"https://jsonplaceholder.typicode.com/albums"
);
setPhotoData(photoData.data);
} catch (error) {
console.log(error);
}
};
getPhotos();
}, []);
useEffect(() => {
//code for finding user. no external api used.
setUser(user);
}
}
}, []);
const passedProps = {
...pageProps,
photoData,
user
};
return (
...
<Component {...passedProps} />
)
Then I pass the data (photodata) from a Home component to a (app.js 's) grandchild component, an Photo component
export default function Home({ photoData, user }) {
return(
<Photo photoData={photoData} user={user} />
)
In Photo component, I am receiving photoData and trying to set a state for photoArr with the default state of photoData.
When the entire app is first loaded, the photoData is passed down to the Photo component successfully that it sets the state without any issue.
But the main problem is that when I am in the Photo page (photos are loaded) and refresh the page, then it does not set the state for photoArr with photoData. Even though I can console log photoData received from app.js, it does not set state, photoArr, with the default state, photoData.
export default function Photo({ photoData, user }) {
const [photoArr, setPhotoArr] = useState(photoData);
//I have this as state because I change this array
//later on in this component (doing CRUD on the photo array).
console.log(photoData); // this returns the array received from app.js
console.log(photoArr); // []. returns an empty array
console.log(user); // returns the user object received from app.js.
return (
<div>
{photoArr.length > 0 ?
.... code mapping photoArr
: "Data is empty" //It returns this comment when I refresh the page
}
</div>
)
As you can see above, when I refresh the page, I get "Data is empty" meaning photoArr was not set even with the given default state. If I keep refreshing the page multiple times, it still shows a blank page.
From my research, it's due to setting state being asynchronous? So then how can I fix this problem?
Try this:
(In your Photo page)
const [photoArr, setPhotoArr] = useState(null);
useEffect(() => {
if(photoData.length) setPhotoArr(photoData) // If not empty, set the Arr
},[photoData]} // We listen to photoData's change
On page load, there aren't any data in your photoData, and as it pass down to Photo component, react remembers that state.
But with useEffect listen to photoData's change, we can setPhotoArr once the getPhotos function got the data back.

Problem with dynamically updating a React Component within a chat project

In my chat application, I am having trouble displaying user messages after send button is pressed.
I receive all the message data from an API and store it inside a state. Here is the relevant part.
I simplified the code because I think the problem is in the logic of my code and there are no syntax errors.
const Chat = (props) => {
const [selectedRoom, setSelectedRoom] = useState({})
const [roomMessages, setRoomMessages] = useState([])
let roomMessagesData = []
function getUserRoomMessages() {
fetch("url").then((response) => {
return response.json();
}).then((requestData) => {
setRoomMessages(requestData.message_list)
});
};
function sendMessage(message) {
fetch("url").then((response) => {
return response.json();
}).then((requestData) => {
getUserRoomMessages()
});
}
function handleSendMessageButton() {
let currentMsg = document.querySelector("#textBox").value;
if (currentMsg != "") {
sendMessage(currentMsg)
roomMessagesData.push(
<div className="message receiver">{currentMsg}</div>
)
}
document.querySelector("#textBox").value = "";
}
useEffect(() => {
getUserRoomMessages()
}, [selectedRoom])
// Note that:
// typeof roomMessages !== 'undefined' will always be true,
// since initially, roomMessages = [], it is defined.
// Instead just check if roomMessages is not empty, to avoid this operation.
if (roomMessages.length > 0) {
const sortedMessages = roomMessages.sort(
(a,b)=> a.message_id > b.message_id ? 1 : -1
);
for (let i = 0; i<sortedMessages.length; i++) {
if (sortedMessages[i].creator_id == user_id) {
roomMessagesData.push(
<div className="message receiver">
{sortedMessages[i].message_text}
</div>
);
} else {
roomMessagesData.push(
<div className="message sender">
{sortedMessages[i].message_text}
</div>
);
}
};
}
return (
<div>
//some code
<div className="chatBox">{roomMessagesData}</div>
<div><input type="text" id="textBox"/></div>
<div>
<input
id="btn"
type="button"
value="Send message"
onClick={handleSendMessageButton}
/>
</div>
</div>
)
}
In this current logic, when I press send message button, the message is sent to the API successfully, but not displayed. Only after I refresh the page new message appear.
All of the calls to API are successful.
My idea is that when a user sends a new message I retrieve a newly updated list of messages from API which then should be re-rendered by reactjs. Or I want push a new element to roomMessagesData so that after it changes reactjs should reload its elements.
The question is what is the problem in my code and what is it that I have do to achieve what I described above.
Quick review of the steps:
So handleSendMessageButton triggers the sendMessage function which does a POST.
And then sendMessage triggers a GET message using getUserRoomMessages.
Within getUserRoomMessages, you trigger state update using setRoomMessages.
And the rendered child {roomMessagesData} is dependant on the changes within roomMessages state.
Based on the above, the issue to be fixed is here:
// ONLY re-renders if selectedRoom changes
useEffect(() => { getUserRoomMessages() }, [selectedRoom]);
Change the above to:
// re-render if roomMessages changes,
// and the message should display automatically
useEffect(() => { getUserRoomMessages() }, [roomMessages]);
Remember, that useEffect() will only run again if certain "dependant" states change.
UPDATE
Despite suggesting the above, notice that it will create infinite loop because - getUserRoomMessages makes a API call and then updates roomMessages state which the useEffect depends on to re-render the component - thus leading to neverending re-rendering and numerous API calls.
Suggestion:
Since getUserRoomMessages is already called when the onClick is triggered, thus updating date... you can instead utilize the useEffect to run only when state is updated e.g.
let loaded = React.useRef(false);
// Cleanup useEffect
useEffect(() => {
// set to true on mount...
isLoaded.current = true;
// ... and to false on unmount
return () => { isLoaded.current = false; };
}, [isLoaded, roomMessages]);
Right now you have defined your list using a variable let roomMessagesData = [],whereas you need to use a state to define it. later on when you change that state , your component will be updated since a state has changed.
another way to go is by deploying a useEffect hook in which you will pass roomMessagesData as the second dependancy. This way , when that array changes, your component will rerender.
useEffect(() => {
const copyRoomMessagesData = [...roomMessagesData]
}, [roomMessagesData])
<div className="chatBox">
{copyRoomMessagesData}
</div>

React Hooks, how to handle dependency If i want to run only when one of them changes?

I wanted to ask if there's a proper way of handling a situation when we have an useEffect that we only care it runs when one variable of the dependency array is changed.
We have a table that has pagination and inputs to filter the content
with a button.
When the user changes the action (inputs) we update this state and
when the user press search we fetch from the api.
When we have results paginated, we then hook on the page and if it
changes we then fetch the corresponding page.
I solved the issue by having the ref of action and checking if the previous value was different from the current value. Though I don't know if this is the best practice
I did something like this.
const FunctionView = () => {
const actionRef = useRef({})
// action object have query params for the api
const fetchData = useCallback((page) => {
// call and api and sets local values
}, [action])
// this hook handle page next or previous
useEffect(() => {
let didCancel = false
if (isEqual(Object.values(action), Object.values(actionRef.current))) {
if (!didCancel) fetchData(page + 1)
}
actionRef.current = action
return () => {
didCancel = true
}
}, [page, fetchData, action])
return (
<>
Components here that changes {action} object, dates and category
<Button onClick={() => fetchData(page)}>Search</Button>
<Table>...</Table>
</>
)
}
I know I can set the dependency array to only page but react lint plugin complains about this so I ended up with warnings.
A common mistake is trying to directly wire the state of forms up to the data they represent. In your example, you DO want to perform the search when action changes. What you're actually trying to avoid is updating action every time one of the inputs changes prior to submit.
Forms are, by nature, transient. The state of the form is simply the current state of the inputs. Unless you really want things to update on every single keystroke, the storage of these values should be distinct.
import { useCallback, useEffect, useState } from 'react'
const Search = () => {
const [action, setAction] = useState({})
const loadData = useCallback(() => {
// call api with values from `action`
}, [action])
useEffect(loadData, [loadData])
return (
<>
<SearchForm setRealData={setAction} />
<Table>...</Table>
</>
)
}
const SearchForm = ({ setRealData }) => {
// don't want to redo the search on every keystroke, so
// this just saves the value of the input
const [firstValue, setFirstValue] = useState('')
const [secondValue, setSecondValue] = useState('')
const handleFirstChange = (event) => setFirstValue(event.target.value)
const handleSecondChange = (event) => setSecondValue(event.target.value)
// now that we have finished updating the form, update the state
const handleSubmit = (event) => {
event.preventDefault()
setRealData({ first: firstValue, second: secondValue })
}
return (
<form onSubmit={handleSubmit}>
<input value={firstValue} onChange={handleFirstChange} />
<input value={secondValue} onChange={handleSecondChange} />
<button type="submit">Search</button>
</form>
)
}

How to detect route changes using React Router in React?

I have a search component that is global to my application, and displays search results right at the top. Once the user does any sort of navigation, e.g., clicking a search result or using the back button on the browser, I want to reset the search: clear the results and the search input field.
Currently, I am handling this with a Context; I have a SearchContext.Provider that broadcasts the resetSearch function, and wherever I am handling navigation, I have consumers that invoke resetSearch before processing navigation (which I do programmatically with the useHistory hook).
This doesn't work for back button presses on the browser's controls (since that is something out of my control).
Is there an intermediate step before my Routes are rendered (or any browser navigation happens) that I can hook into, to make sure my resetSearch function is invoked?
As requested, here is the code:
// App.js
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const resetSearch = () => {
setResults([]);
setQuery("");
};
<SearchContext.Provider value={{ resetSearch }}>
// all of my <Route /> components in this block
</SearchContext.Provider>
// BusRoute.js
const { resetSearch } = useContext(SearchContext);
const handleClick = stop => {
resetSearch();
history.push(`/stops/${stop}`);
};
return (
// some other JSX
<button onClick={() => handleClick(stop.code)}>{stop.name}</button>
);
You can listen to history changes:
useEffect(() => {
const unlisten = history.listen((location) => {
console.log('new location: ', location)
// do your magic things here
// reset the search: clear the results and the search input field
})
return function cleanup() {
unlisten()
}
}, [])
You can use this effect in your parent component which controls your global search's value.
You can use componentWillUnmount feature from class components with useEffect in hooks with functional component

Resources