Initializing component with parameter from local storage in react/nextjs - reactjs

I have a component provided by an external library which I need to provide an access token to. This access token is saved in localStorage at this point and I would like to read it and pass it to the component. This is my attempt using useEffect but this fails with the error {code: "initialization-error", error: "Access token required"}
const test: NextPage = () => {
const router = useRouter()
let accessToken = ''
useEffect(() => {
const at = localStorage.getItem('accessToken')
console.log(at) // prints out the correct access token
if (at !== null) {
accessToken = at
}
}, [])
return (
<WebSdk
accessToken={accessToken}
expirationHandler={() => Promise.resolve(accessToken)}
onError={(error) => {
console.log(error)
}}
/>
)
}
export default test
I'm guessing the useEffect hook runs after the component renderes - how can I read from local storage before or as the component renders?

Since you are using nextjs you can't call the localStorage directly, so you have to check if the bowser has "initialized", something like this:
const test: NextPage = () => {
const router = useRouter()
const [initialized, setInitialized] = useState(false);
const [accessToken, setAccessToken] = useState('');
useEffect(() => {
const at = localStorage.getItem('accessToken') || '';
setInitialized(true);
setAccessToken(at);
}, [])
if (!initialized) {
return null;
}
return (
<WebSdk
accessToken={accessToken}
expirationHandler={() => Promise.resolve(accessToken)}
onError={(error) => {
console.log(error)
}}
/>
)
}
export default test
I'm guessing the useEffect hook runs after the component renderes
Yes the component will render first before the hooks can be triggered, this is the equivalent of "componentDidMount" (if the hook's second parameter is empty) in early verions of react.

Related

when i reload the page local storage will be empty so i want to store data after reloading in react

const [contacts, setContacts] = useState([]);
//Fetch the Value from Local Storage
useEffect(() => {
const retrieveContacts = localStorage.getItem(LOCAL_STORAGE_KEY);
if (retrieveContacts) {
setContacts(JSON.parse(retrieveContacts));
}
}, []);
//Storage the Value
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts]);
i tried to fetch data from local storage but it's not working
The use of useEffect hooks is the issue. At the second useffect setting the 'contacts' s not happeing since the first one hasn't yet been set. So the value of setting the localstorage goes null. Instead have the value set at the point of useState hooks. Also, when you are using localStorage, it's important to handle the initial state as well. Try the code below.
import React from 'react'
import { useState, useEffect } from 'react';
const App2 = () => {
const LOCAL_STORAGE_KEY = 'key-1'
const [contacts, setContacts] = useState(
localStorage.getItem(LOCAL_STORAGE_KEY) ? JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) : {name:'james', count:0}
);
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts]);
const handleButtonClickEvent = () => {
setContacts({name:'james', count:contacts.count + 1})
}
return (
<>
<div>Name: {contacts.name}</div>
<div>Country: {contacts.count}</div>
<input type='button' value='button' onClick={(e) => handleButtonClickEvent()}></input>
</>
)
}
export default App2
Your local storage will be empty after every reload because your default 'contacts' value is an empty array. Your second useEffect then saves this empty array to local storage when the component mounts, overwriting whatever value was previously saved there.
You can resolve this by getting rid of your first useEffect, and loading the default 'contacts' value from local storage:
const retrieveContacts = () => localStorage.getItem(LOCAL_STORAGE_KEY) || []; // Outside of component
...
const [contacts, setContacts] = useState(() => retrieveContacts()); // Inside of component
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts]);

React+Jest Testing state change in useEffect

So I've a similar useCase defined in stackover flow answer, but somewhat different. I have a react component and it has two useEffect hooks. One useEffect hook is used at first time render and other once the dependency in that useEffect hook changes. I tried mocking getDelivery but it comes from a custom hook defined in app.
// App.tsx
const App = () => {
const [deliveryType, setDeliveryType] = useState([])
const { DeliveryStatusService } = useServices()
const deliveryStatusService = new DeliveryStatusService()
const { deliveryId } = useParams()
useEffect(() => {
setup()
}, [])
useEffect(() => {
if (deliveryType) {
console.log("DeliveryType is Set")
}
}, [deliveryType])
const setup = async () => {
const response = await deliveryStatusService.getDelivery(deliveryId) // this call comes from custom hook and external service
setDeliveryType(response)
}
return (
<>
<div>{!deliveryType ? <div>Render Loading</div> : <div>Data loaded with {deliveryType}</div>}</div>
</>
)
}
I tried to mock it out as stated in above article as follows using Jest and Enzyme but if I console.log(wrapper.debug) I still get Render Loading and not Data loaded with {deliveryType}.
I tried mocking it like this :
const getDelivery = jest.fn()
const DeliveryStatusService = jest.fn().mockImplementation(() => ({
getDelivery,
}))
it('mock getDelivery', () => {
const wrapper = mount(<ServiceProvider services={{DeliveryStatus}}>
<App />
</ServiceProvider>
getDelivery.mockReturnValue(
Promise.resolve(
... dummyData
),
)
await act(flushPromises)
wrapper.update()
await act(flushPromises)
console.log(wrapper.debug())
console.log(wrapper.debug()) yields the following output:
<ServiceProvider services={{...}}>
<App>
<div>
<div>
Render Loading
</div>
</div>
<App>
</ServiceProvider>
Am I not mocking correctly so that I would never have Data loaded div?

Reload component with react hooks

I would like to ask you how to reload a component after modifying the data of a form, then I have my component:
export default function MyComponent() {
const url = "/api/1";
const [resData, setResData] = useState(null);
useEffect(() => {
const jwt = getJwt();
const fetchData = async () => {
const resP = await axios(url);
setResData(resP.data);
};
fetchData();
}, []);
return <EditComponent={resData} />
}
This component passes my data to the "EditCompoent" child component in which there is a form that is filled with data from the parent component that I can modify in which there is a save button that when I click allows me to send the modified data to my beckend:
const handleConfirm = () => {
axios.put(url, data).then((res) => {
//Reload Component
})
}
I would like to be able to reload the parent component as soon as this works is successful what could I do? I don't want to reload the whole page I just want to reload the parent component that is "MyComponent", I hope I have well posed the problem.
I'd pass the whole useEffect callback down so that handleConfirm can call it again after the axios.put, after which the resData state in the parent will be updated:
export default function MyComponent() {
const url = "/api/1";
const [resData, setResData] = useState(null);
const tryLoginJWT = () => {
const jwt = getJwt();
const resP = await axios(url);
setResData(resP.data);
};
useEffect(tryLoginJWT, []);
return <EditComponent {...{ resData, tryLoginJWT }} />
}
const handleConfirm = () => {
axios.put(url, data)
.then(tryLoginJWT)
.catch(handleErrors); // don't forget to catch here in case there's a problem
}

Current user with React Context

I want to have a way to get and fetch the current user using React Context (to not pass props to all my components)
I tried using React Context but didn't understand how I would achieve something like const { currentUser, fetchCurrentUser } = useCurrentUser() from the docs.
here is what i did for my project:
// src/CurrentUserContext.js
import React from "react"
export const CurrentUserContext = React.createContext()
export const CurrentUserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = React.useState(null)
const fetchCurrentUser = async () => {
let response = await fetch("/api/users/current")
response = await response.json()
setCurrentUser(response)
}
return (
<CurrentUserContext.Provider value={{ currentUser, fetchCurrentUser }}>
{children}
</CurrentUserContext.Provider>
)
}
export const useCurrentUser = () => React.useContext(CurrentUserContext)
and then use it like this:
setting up the provider:
// ...
import { CurrentUserProvider } from "./CurrentUserContext"
// ...
const App = () => (
<CurrentUserProvider>
...
</CurrentUserProvider>
)
export default App
and using the context in components:
...
import { useCurrentUser } from "./CurrentUserContext"
const Header = () => {
const { currentUser, fetchCurrentUser } = useCurrentUser()
React.useEffect(() => fetchCurrentUser(), [])
const logout = async (e) => {
e.preventDefault()
let response = await fetchWithCsrf("/api/session", { method: "DELETE" })
fetchCurrentUser()
}
// ...
}
...
the full source code is available on github: https://github.com/dorianmarie/emojeet
and the project can be tried live at: http://emojeet.com/
If useContext returns undefined, then you might have forgotten the Provider or need to move your provider up the stack.
U dont explained what u want to do with the data but...After u exec the function fetch in use effect.
Now u have the object user in the state currentUser.
Try to console log after the use effect the currentUser and see what dat inside it.
After u can use it with currentUser."whatever prop inside"..

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