update data only if state changes with react - reactjs

I have a User component and I want to make an api call to update the inactive state.
The problem that I´m having is that the useEffect is being called when the component receives props. That means it will make the call when component renders for the first time. What´s the pattern to avoid this and only make the call if inactive state changes?
My initial try was to remove user from the dependencies array. But I need to get the user.id to pass it to the service.
What´s the pattern to avoid making these unnecessary calls?
const User = (props) => {
const { user } = props
const [inactive, setInactive] = useState(user.inactive);
useEffect(() => {
const abortController = new AbortController();
const data = { inactive: Number(inactive) }
updateUserRecordInDatabase(user.id, data);
return () => {
abortController.abort();
};
}, [inactive, user])
return (
<button onClick={ () => setInactive(!inactive) } />toggle { user.name } state</button>
)
}

You can try:
const User = (props) => {
const { user } = props
const [inactive, setInactive] = useState(user.inactive);
const [numUpdate, setNumUpdate] = useState(0);
const {id} = user //You get id of use here
useEffect(() => {
if(numUpdate === 0){
// This is first time of useEffect
setNumUpdate(numUpdate + 1); // Only update when first time of useEffect
}
const abortController = new AbortController();
const data = { inactive: Number(inactive) }
updateUserRecordInDatabase(id, data);
return () => {
abortController.abort();
};
}, [inactive, id])
return (
<button onClick={ () => setInactive(!inactive) } />toggle { user.name } state</button>
)
}

Related

How to reuse the same functionality in useEffect and another action inside component?

I want to reuse the function resolveSessionData() inside a component. It is used to set some state on a prop change as well as in a user action. Is this the correct way to reuse the functionality or is there any better way in React?
const { useEffect, useState } = require("react");
const Component = ({someProp}) => {
const [itemData, setItemData] = useState(null);
useEffect(() => {
if (someProp) {
resolveSessionData();
}
}, [someProp]);
const resolveSessionData = () => {
const data = database.getItemData();
setItemData(data);
};
return <div>
{/* show item data */}
<button onClick={resolveSessionData}>
Activate Lasers
</button>
</div>;
};
To reuse the functionality of useEffect and other functions, here you may use custom hook and cover the common functionality in the custom hook and re-use in component where you need.
ex.
import React, { useCallback, useEffect, useState } from "react";
// ---------------------------------- Custom Hook -----------------------------
const useCustomFetch = ({someProp}) => {
const [itemData, setItemData] = useState(null);
const resolveSessionData = useCallback(() => {
const data = database.getItemData(); // fetching data
setItemData(data); // setting fetched data to state
}, []);
useEffect(() => {
if (someProp) {
resolveSessionData(); // or may be fetch data based on props
}
}, [someProp]);
const addNewItem = useCallback(function (dataToAdd) {
/**
* write code to add new item to state
*/
}, [])
const removeExistingItem = useCallback(function (dataOrIndexToRemove) {
/**
* write code to remove existing item from state
*/
}, [])
return {
itemData,
addNewItem,
removeExistingItem,
}
}
// ---------------------------------- Component managing users -----------------------------
const ComponentUser = ({someProp}) => {
const {
addNewItem,
itemData,
removeExistingItem
} = useCustomFetch('users')
const handleAction = useCallback(function (action, data) {
if (action === 'add') {
addNewItem(data) // ex. adding user
} else if (action === 'remove') {
removeExistingItem(data) // ex. removing user
}
}, [])
return <div>
{itemData.map(function () {
/**
* render items here // ex. listing users
*/
})}
<button onClick={handleAction}>
Add / Remove
</button>
</div>;
};
// ---------------------------------- Component managing posts -----------------------------
const ComponentPosts = ({someProp}) => {
const {
addNewItem,
itemData,
removeExistingItem
} = useCustomFetch('posts') // re-using custom hook
const handleAction = useCallback(function (action, data) {
if (action === 'add') {
addNewItem(data) // ex. add new post
} else if (action === 'remove') {
removeExistingItem(data) // ex. remove existing post
}
}, [])
return <div>
{itemData.map(function () {
/**
* render items here // ex. render posts
*/
})}
<button onClick={handleAction}>
Activate Lasers
</button>
</div>;
};
Yes, it is correct way. There is no better way in React.
You'r doing right , but if you introduce any state value into resolveSessionData go with useCallback version (commented code)
import React, { useState, useEffect, useCallback } from "react";
export default function App({ someProp }) {
const [itemData, setItemData] = useState(null);
// const resolveSessionData = useCallback((e) => {
// e.preventDefault();
// const data = database.getItemData();
// setItemData(data);
// },[])
useEffect(() => {
if (someProp) {
resolveSessionData();
}
}, [someProp]);
const resolveSessionData = (e) => {
e.preventDefault();
const data = database.getItemData();
setItemData(data);
};
return (
<div>
{/* show item data */}
<button onClick={resolveSessionData}>Activate Lasers</button>
</div>
);
}
from my viewpoint, the props should be used in the constructor useEffect :
const { useEffect, useState } = require("react");
const Component = ({someProp}) => {
const [itemData, setItemData] = useState(null);
useEffect(() => { // constructor
if (someProp) {
resolveSessionData();
},[]); // <= []
const resolveSessionData = () => {
const data = database.getItemData();
setItemData(data);
};
return <div>
{/* show item data */}
<button onClick={resolveSessionData}>
Activate Lasers
</button>
</div>;
};

How to use useNavigate outside react hook?

Gets list of emails from firestore and checks if current user is registered and then redirects them to sign up if they are new user.
The code is functional(it redirects succesfully) but get the following error:
arning: Cannot update a component (BrowserRouter) while rendering a different component You should call navigate() in a React.useEffect(), not when your component is first rendered.
const navigate = useNavigate();
let hasEmail = false;
const [emailList, setEmailList] = useState([]);
const emailRef = collection(db, "emails");
useEffect(() => {
const getEmails = async () => {
const data = await getDocs(emailRef);
setEmailList(
data.docs.map((doc) => ({
...doc.data(),
}))
);
};
getEmails();
}, []);
const emailCheck = (emails) => { //checks if email exists
hasEmail = emails.some((e) => e.email === auth.currentUser.email);
};
const direct = () => { // redirects to required page
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
emailCheck(emailList);
direct();
Move the email checking logic into a useEffect hook with a dependency on the emailList state.
const navigate = useNavigate();
const [emailList, setEmailList] = useState([]);
const emailRef = collection(db, "emails");
useEffect(() => {
const getEmails = async () => {
const data = await getDocs(emailRef);
setEmailList(
data.docs.map((doc) => ({
...doc.data(),
}))
);
};
getEmails();
}, []);
useEffect(() => {
if (emailList.length) {
const hasEmail = emailList.some((e) => e.email === auth.currentUser.email);
navigate(hasEmail ? "/index" : "/enterdetails");
}
}, [auth, emailList, navigate]);
This might not run without the proper firebase config but check it out
https://codesandbox.io/s/elated-bell-kopbmp?file=/src/App.js
Things to note:
Use useMemo for hasEmail instead of emailCheck. This will re-run only when emailList changes
const hasEmail = useMemo(() => {
//checks if email exists
return emailList.some((e) => e.email === auth.currentUser.email);
}, [emailList]);
There isn't really a point in having this in a react component if you are just redirecting away. Consider having the content of 'index' at the return (</>) part of this component. Only redirect if they aren't authorized
useEffect(() => {
if (!hasEmail) {
navigate("/enterdetails");
}
//else {
// navigate("/index");
//}
}, [hasEmail, navigate]);

Can I update state inside nested function in react functional component?

function App(){
const [listState, setListState] = useState([]);
let connectWebsocket = () => {
let ws = new WebSocket(`ws://${window.location.host}/ws`);
ws.onmessage = (event) => {
let data = JSON.parse(event.data);
setListState([...listState, data]);
};
}
useEffect(() => {
console.log(listState); // Problem: This always logs empty array.
}, [listState]);
useEffect(() => {
connectWebsocket();
}, []);
return (
<div>
</div>
);
}
When I update state in nested functions, the state is updated to an initial value.
How to fix this problem?

React async event handler. The safe way

Imagine a simple react component
const Upload: React.FC = () => {
const [done, setDone] = useState(false)
const upload = async () => {
await doSomeAsyncStuffHere()
setDone(true)
}
if(done) {
return <div>success</div>
}
return (
<button onClick={upload}>upload</button>
)
}
It looks fine at first glance. But what if upload function takes a long time to finish? What if user navigates to another view and the component gets unmounted? When the async task finishes will cause a state update on an unmounted component. This is a no-op and a possible memory leak. What should I do to prevent it?
One way of going about it is to create a ref that you set to false when the component is unmounted, and check against this before setting the result of your asynchronous code in the component state.
Example
const Upload: React.FC = () => {
const isMounted = useRef(true);
const [done, setDone] = useState(false)
const upload = async () => {
await doSomeAsyncStuffHere()
if (isMounted.current) {
setDone(true)
}
}
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
if(done) {
return <div>success</div>
}
return (
<button onClick={upload}>upload</button>
)
}

How to send request on click React Hooks way?

How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?
What i see so far is to have something "indirect" like:
export default = () => {
const [sendRequest, setSendRequest] = useState(false);
useEffect(() => {
if(sendRequest){
//send the request
setSendRequest(false);
}
},
[sendRequest]);
return (
<input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
);
}
Is that the proper way or is there some other pattern?
export default () => {
const [isSending, setIsSending] = useState(false)
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
this is what it would boil down to when you want to send a request on click and disabling the button while it is sending
update:
#tkd_aj pointed out that this might give a warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.
export default () => {
const [isSending, setIsSending] = useState(false)
const isMounted = useRef(true)
// set isMounted to false when we unmount the component
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
if (isMounted.current) // only update if we are still mounted
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method
const App = (props) => {
//define you app state here
const fetchRequest = useCallback(() => {
// Api request here
}, [add dependent variables here]);
return (
<input type="button" disabled={sendRequest} onClick={fetchRequest}
);
}
Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop
In functional programming, any async function should be considered as a side effect.
When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).
Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.
Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.
to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted
/**
* check if the component still mounted
*/
export const useIsMounted = () => {
const mountedRef = useRef(false);
const isMounted = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return isMounted;
};
Then your code should look like this
export const MyComponent = ()=> {
const isMounted = useIsMounted();
const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);
// do my async thing
const doMyAsyncThing = useCallback(async () => {
// do my stuff
},[])
/**
* do my async thing effect
*/
useEffect(() => {
if (isDoMyAsyncThing) {
const effect = async () => {
await doMyAsyncThing();
if (!isMounted()) return;
setIsDoMyAsyncThing(false);
};
effect();
}
}, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);
return (
<div>
<button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
Do My Thing {isDoMyAsyncThing && "Loading..."}
</button>;
</div>
)
}
Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)
UPDATE:
Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.
Example:
import {useAsyncFn} from 'react-use';
const Demo = ({url}) => {
const [state, doFetch] = useAsyncFn(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
<button onClick={() => doFetch()}>Start loading</button>
</div>
);
};
You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.
Example
const { useState } = React;
function getData() {
return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}
function App() {
const [data, setData] = useState(0)
function onClick() {
getData().then(setData)
}
return (
<div>
<button onClick={onClick}>Get data</button>
<div>{data}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:
const [requestSent, setRequestSent] = useState(false);
const sendRequest = () => {
setRequestSent(true);
fetch().then(() => setRequestSent(false));
};
Working example
You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).
useApi hook:
export type ApiMethod = "GET" | "POST";
export type ApiState = "idle" | "loading" | "done";
const fetcher = async (
url: string,
method: ApiMethod,
payload?: string
): Promise<any> => {
const requestHeaders = new Headers();
requestHeaders.set("Content-Type", "application/json");
console.log("fetching data...");
const res = await fetch(url, {
body: payload ? JSON.stringify(payload) : undefined,
headers: requestHeaders,
method,
});
const resobj = await res.json();
return resobj;
};
export function useApi(
url: string,
method: ApiMethod,
payload?: any
): {
apiState: ApiState;
data: unknown;
execute: () => void;
} {
const [apiState, setApiState] = useState<ApiState>("idle");
const [data, setData] = useState<unknown>(null);
const [toCallApi, setApiExecution] = useState(false);
const execute = () => {
console.log("executing now");
setApiExecution(true);
};
const fetchApi = useCallback(() => {
console.log("fetchApi called");
fetcher(url, method, payload)
.then((res) => {
const data = res.data;
setData({ ...data });
return;
})
.catch((e: Error) => {
setData(null);
console.log(e.message);
})
.finally(() => {
setApiState("done");
});
}, [method, payload, url]);
// call api
useEffect(() => {
if (toCallApi && apiState === "idle") {
console.log("calling api");
setApiState("loading");
fetchApi();
}
}, [apiState, fetchApi, toCallApi]);
return {
apiState,
data,
execute,
};
}
using useApi in some component:
const SomeComponent = () =>{
const { apiState, data, execute } = useApi(
"api/url",
"POST",
{
foo: "bar",
}
);
}
if (apiState == "done") {
console.log("execution complete",data);
}
return (
<button
onClick={() => {
execute();
}}>
Click me
</button>
);
For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.
<const Component= (props) => {
//define you app state here
const getRequest = useCallback(() => {
// Api request here
}, [dependency]);
return (
<input type="button" disabled={sendRequest} onClick={getRequest}
);
}
My answer is simple, while using the useState hook the javascript doesn't enable you to pass the value if you set the state as false. It accepts the value when it is set to true. So you have to define a function with if condition if you use false in the usestate

Resources