I'm trying to fetch data from Spotify API. Since I need an access token to do this, I built a custom hook to parse the token from the URL that comes from the server.
I also have another custom hook with the actual request to the API that takes the parsed token as an argument. Both are gathered in a parent hook.
I cannot make this work since the token is never reaching the scope of the request hook so it fails. If I parse the token and make the request within the same hook everything works out just fine. I intended to make a hook for every request since it's not just one, that's why I wanted to pass the token as an argument.
Token custom hook
export default () => {
const [ token, setToken ] = useState('')
useEffect(() => {
const { access_token } = queryString.parse(window.location.search)
return setToken(access_token)
}, [])
return token
}
Request hook
export default function useFetchUserData(token) {
//state
const initialUserState = {
display_name: '',
external_url: '',
images: ''
}
const [ userData, setUserData ] = useState(initialUserState)
const [ isLoading, setIsLoading ] = useState(false)
const [ isError, setIsError ] = useState(false)
useEffect(() => {
async function getUserData() {
setIsLoading(true);
setIsError(false);
const spotify = axios.create({
baseURL: 'https://api.spotify.com/v1/me',
headers: {
'Authorization': `Bearer ${token}`
}
})
try {
const userRes = await spotify('/')
.then( res => { return res.data });
setUserData({
display_name: userRes.display_name,
external_url: userRes.external_urls.spotify,
images: userRes.images[0].url
})
} catch (error) {
setIsError(true)
}
setIsLoading(false);
}
getUserData();
}, [])
const data = {
userData,
isLoading,
isError
}
return data
}
Parent hook
export default function Home() {
const token = useParseToken()
const { userData, isLoading, isError } = useFetchUserData(token);
if (isLoading) return <BarLoader />;
if (isError) return <div>Oops! something went wrong</div>;
return (
<Fragment>
<Header userData={userData}/>
</Fragment>
)
}
What happens in your case is that you are setting a state in useEffect hook in your custom hook to set token. However you return the token from this hook without waiting for the effect to run, so the first time your useFetchUserData hook is called, it will receive empty string as a token. To solve this, you must implement the useFetchUserData hook to run once token is available or it changed
export default function useFetchUserData(token) {
//state
const initialUserState = {
display_name: '',
external_url: '',
images: ''
}
const [ userData, setUserData ] = useState(initialUserState)
const [ isLoading, setIsLoading ] = useState(false)
const [ isError, setIsError ] = useState(false)
useEffect(() => {
async function getUserData() {
setIsLoading(true);
setIsError(false);
const spotify = axios.create({
baseURL: 'https://api.spotify.com/v1/me',
headers: {
'Authorization': `Bearer ${token}`
}
})
try {
const userRes = await spotify('/')
.then( res => { return res.data });
setUserData({
display_name: userRes.display_name,
external_url: userRes.external_urls.spotify,
images: userRes.images[0].url
})
} catch (error) {
setIsError(true)
}
setIsLoading(false);
}
if(token !== '' || token !== null) {
getUserData();
}
}, [token])
const data = {
userData,
isLoading,
isError
}
return data
}
Also since useParseToken returns the token, you don't need to destructure it while using
const token = useParseToken();
You have to use createContext api of react.
Save your token as a context. and use it where ever you want.
I think this repository will help you.
Related
i want to fetch datas from backend with axios Authorization header.
I get token code from local storage and set it to a state.
for first load every thing is ok and datas are showin correctly.but on each render I got 401 (Unauthorized) error.
here is my code. where is the problem?
const UserManage = () => {
const [tokenCode, setTokenCode] = useState("");
const api_url = "http://localhost:5000";
const accessToken = tokenCode;
const AuthAxios = axios.create({
baseURL: api_url,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
useEffect(() => {
const loginDetail = JSON.parse(localStorage.getItem("authState"));
setTokenCode(loginDetail.token); //access token code from local storage
}, []);
const [users, setusers] = useState([])
useEffect(() => {
const fetchUsers = async() => {
const {
data
} = await AuthAxios.get(`/user/all`);
setUsers(data);
setUsers(data);
};
try {
fetchUsers();
} catch (error) {
console.log(error);
}
}, []);
}
I'm getting a 400 client error and saying state.category.push is not a function and the current state is pending. I will be happy if someone helps me out on this. The category array is not accepting the data coming into it. I have both the form file and the reducer file down there. I am trying to create a category array to accepts users category and later output it out.
blogSlice.js
My Reducer file
const urlParams = new URLSearchParams(window.location.search)
const TokenAuthless = urlParams.get('enter')
if(TokenAuthless){localStorage.setItem('authless',
JSON.stringify(TokenAuthless))}
var Token = JSON.parse(localStorage.getItem("authless"))
const initialState = {
blogItems: [],
isLoading: null,
category: [{}],
authors: [],
}
const categoryUrl = 'api/v1/admin/createBlogCat';
var api_origin = 'xxxxxxxxxxxx'
export const insertCategory = createAsyncThunk('blog/insertCategory', async(data,
thunkAPI) => {
const{ rejectWithValue } = thunkAPI;
try {
const res = await fetch(`${api_origin}/${categoryUrl}`,{
method :'POST',
mode: 'cors',
body: JSON.stringify({data}),
headers : {
'Authorization': `Bearer ${Token}`,
'Content-type':'application/json',
'Accept':'application/json',
'Access-Control-Allow-Origin':'*',
},
})
const catData = await res.json();
return catData.data;
} catch (error) {
return rejectWithValue(error.message)
}
})
[insertCategory.pending]:(state, action) => {
state.isLoading = true;
},
[insertCategory.fulfilled]: (state, action) => {
state.isLoading = false;
console.log(action.payload);
console.log(current(state))
state.category.push(action.payload);
console.log('the state category', state.category);
},
[insertCategory.rejected]:( state, action ) => {
state.isLoading = false;
},
CreateCategory.js
Creating a form to accept the input here
const CreateCatAut = () => {
const [name, setName] = useState('');
const dispatch = useDispatch()
const handleSubmit=(e)=>{
e.preventDefault();
const categoryData = {name}
dispatch(insertCategory(categoryData));
console.log(categoryData)
}
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Active Input"
value={name}
onChange={(e)=> setName(e.target.value)} />
)
It means that your state.category is not an array. At
state.category.push(action.payload)
I assigned a value to the state.category which is not yet an array. By first creating an array and then putting that element into it.
state.category= [action.payload]
and at the next iterations, i will have an array with one element and can use push on it.
I've started using custom hook's in React, creating the following:
export function useLazyHook({ onCompleted, onError }) {
// Apollo
const client = useApolloClient()
/**
* Use State
*/
const [loading, setLoading] = useState(false)
const [data, setData] = useState(null)
const [error, setError] = useState(null)
/**
* Method
*/
const CallMe = async ({ input }) => {
// Loading
setLoading(true)
try {
const data = await client.mutate({
mutation: MUTATION,
variables: {
Input: input,
},
})
setLoading(false)
setData(data)
return { error: null, data: data }
} catch (error) {
setError(error)
if (error.graphQLErrors) {
setError(error.graphQLErrors[0])
return { error: error.graphQLErrors[0], data: null }
}
}
}
// Return
return [{ loading, data, error }, CallMe]
}
The hook can be using in the following ways:
const [{ loading, data, error }, CallMe] = useLazyHook({
onCompleted(res) {
console.log(res)
},
onError(err) {
console.log(err)
},
})
We can access the loading, data and error var the declared variables or within the onCompleted and onError. We can also access the same data inline via:
const { error, data } = await CallMe({
input: {},
})
console.log(error)
console.log(data)
All the above works fine, however, if there is something I'm missing or doing incorrectly then any advice is more than welcome.
I have multiple API calls with fairly lengthy, yet similar, response/error handling for each call.
What is the best non-repetitive ways to make multiple independent api calls that update state using fetch?
Copying and pasting 40+ instances of fetch doesn't seem right.
I want to avoid doing this ....
fetch(url,options)
.then((response) => {
// ...
return response.json
})
.then((data) => {
setState(data)
//...
})
.catch((err) => {
//Error logic here
})
Here's what I've done so far:
I made (found and modified) a useFetch hook...
useFetch.ts
//Only calls fetch() when .load() is called.
const useFetch = (path : string, HttpMethod : string, dependencies : any = [] , body : {} | undefined = undefined) => {
const history = useHistory()
const [response, setResponse] = useState<any>({});
const [error, setError] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [controller, setController] = useState(2)
const [isReady, setIsReady] = useState<any>(false)
const load = ():void => {
setError("")
//This prevents useEffect from triggering on declaration.
if (isReady) {
//Math.random() is just to get useEffect to trigger.
setController(Math.random())
}
}
const token = localStorage.getItem("token");
let requestOptions:any = {
method: HttpMethod,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "* always",
Authorization: "Token " + token,
},
};
if (body !== undefined) {
requestOptions["body"] = {
body: JSON.stringify(body)
}
}
const URI = BASE_URI + path
useEffect(() => {
const fetchData = async () => {
if (controller !== 2) {
setIsLoading(true);
try {
const res = await fetch(URI, requestOptions);
const json = await res.json();
if (json?.action == "ENFORCE_BILLING" ) {
history.push(BILLING_CREDENTIALS_PATH, { enforceBillingPopUp: true });
}
if (json?.action == "ENFORCE_SMS_CONFIRMATION") {
// Should we log user out, as well?
history.push(CONFIRMATION_CODE_PATH)
}
if (res.ok) {
setResponse(json);
setIsLoading(false)
} else {
setError(json)
setIsLoading(false)
}
} catch (err) {
setError(err);
// Error logic here...
}
}
}
};
fetchData()
setIsReady(true)
}, [controller, ...dependencies]);
return { response, setResponse ,error, isLoading, load, isReady };
};
Component.tsx
//Inside react functional component...
// Prepares to fetch data from back-end
const data1 = useFetch(PATH1, "GET");
const data2 = useFetch(PATH2, "GET");
const data3 = useFetch(PATH3, "GET");
useEffect(() => {
// Initial on load data fetch
// .load() fetches data
data1.load();
data2.load();
data3.load();
}, [activeReservations.isReady]);
// Sort data depending on sort selection
...
Is useFetch considered bad practice? What are the advantages of using Redux, instead?
Any help would be greatly appreciated. Thanks.
What I am trying to do
I have a lobby that users can join. To persist the joined lobby on the client on a page refresh I decided to put the lobby that has been joined into the browser's session storage. Before it was just in a useState which doesn't persist through a page refresh.
Setting Session Storage is classified as a side effect as far as I know and should be handled in useEffect. The problem is when I set the lobby the useEffect that has the lobby as a dependency doesn't run.
Setting breakpoints shows that it doesn't run at all, but I can see that the joinedLobby has changed from undefined to an object (example : {success: "Successfully joined ...", payload : { id:"", ...}}).
The session store stays empty.
Code Sandbox
Sandbox
CSS is broken since I was using Emotion
Update
Fetching Data from the back end breaks the app. Making the data static made the app function like it should.
I have 0 ideas on why / how. The culprit seems to be play_index.jsx at line 165 const jsonResponse.
Setting the state that should update the useEffect
const { setJoinedLobby } = useContext(JoinedLobbyProviderContext);
const history = useHistory();
useEffect(() => {
if (joinState.result === undefined) return;
setJoinedLobby(joinState.result);
history.push('/lobby');
}, [joinState.result, history, setJoinedLobby]);
Provider inside router
<JoinedLobbyProviderContext.Provider
value={{ getJoinedLobby, setJoinedLobby }}>
<Route path='/play'>
<Play />
</Route>
<Route path='/lobby'>
<Lobby />
</Route>
</JoinedLobbyProviderContext.Provider>
The functions the provider takes
const [joinedLobby, setJoinedLobby] = useState(undefined);
useEffect(() => {
if (joinedLobby === undefined) return;
sessionStorage.setItem('joinedLobby', JSON.stringify(joinedLobby));
}, [joinedLobby]);
const getJoinedLobby = () => {
return JSON.parse(sessionStorage.getItem('joinedLobby'));
};
Edit : How joinState.result changes
const joinInit = {
errors: undefined,
loading: false,
result: undefined,
id: undefined,
};
const joinReducer = (state, action) => {
switch (action.type) {
case 'joinLobby': {
return { ...state, id: action.payload };
}
case 'loadingTrue':
return { ...state, loading: true };
case 'setResult':
return { ...state, loading: false, result: action.payload };
case 'setErrors':
return {
...state,
loading: false,
errors: action.payload,
};
case 'reset':
return joinInit;
default : {throw new Error('Didn't find action passed to reducer')}
}
};
const [joinState, joinStateDispatch] = useReducer(joinReducer, joinInit);
const passRef = useRef();
useEffect(() => {
const joinLobby = async () => {
joinStateDispatch({ type: 'loadingTrue' });
try {
const jsonResponse = await (
await fetch(`${BACKEND_URL}/play/joinLobby/${joinState.id}`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
password: passRef.current.value,
}),
})
).json();
joinStateDispatch({ type: 'setResult', payload: jsonResponse });
} catch (e) {
joinStateDispatch({ type: 'setErrors', payload: e });
}
};
if (joinState.id !== undefined) {
joinLobby();
}
}, [joinState.id, joinStateDispatch]);