I'm trying to optimize performance of my app and was thinking about implementing axios interceptors.
And trying to do so, I run into problem that I can not understand.
Here is my App.js file:
function App() {
const Stack = createStackNavigator();
const initialLoginState = {
isLoading: true,
userToken: null,
};
const loginReducer = (prevState, action) => {
switch (action.type) {
case 'RETRIEVE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'LOGIN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'LOGOUT':
return {
...prevState,
userToken: null,
isLoading: false,
};
}
};
const [loginState, dispatch] = useReducer(loginReducer, initialLoginState);
const authContext = useMemo(
() => ({
signIn: async (userToken) => {
await setStoredItem('token', userToken);
dispatch({type: 'LOGIN', token: userToken});
},
signOut: async () => {
await deleteStoredItem('token');
dispatch({type: 'LOGOUT'});
},
}),
[],
);
useEffect(() => {
setTimeout(async () => {
let userToken = null;
let token = await getStoredItem('token');
if (token != undefined && token.length > 0) {
userToken = token;
}
dispatch({type: 'RETRIEVE_TOKEN', token: userToken});
}, 1000);
}, []);
axios.interceptors.request.use((config) => {
console.log(
'loginState.userToken INSIDE intercetor',
loginState.userToken,
);
// request.headers.Authorization = loginState.userToken ? `Bearer ${userToken}` : '';
return config;
});
console.log('state OUTSIDE intercetor', loginState.userToken);
As you can see I'm using useReducer react hook for Authentication state management and then I conditionally render routes depending if loginState.userToken !== null or not.
I was trying to add loginState.userToken to interceptor (commented out code) and then something weird happens - State value seem to change and I dont know why.
See screenshot below:
At first it accurate (actual token), but then it changes to null?
What am I missing here?
Thank you!
Related
const redirectURL = 'http://localhost:3000/';
const authorizePage = `https://accounts.spotify.com/authorize?response_type=token&client_id=${clientID}`
+`&redirect_uri=${redirectURL}&scope=playlist-modify-public`;
export const searchTerm = createAsyncThunk('musics/searchTerm',
async (term, {dispatch, getState}) => {
let accessToken = getState().result.accessToken;
if (!accessToken) {
window.location.replace(authorizePage);
let tempToken = /access_token=(.*?)&/.exec(window.location.href);
let expireTime = /expires_in=(.*)/.exec(window.location.href);
dispatch(setAccessToken(tempToken[1]));
window.setTimeout(() => {
console.log('here is mistake right?');
dispatch(setAccessToken(''));
}, Number(expireTime[1]) * 1000);
window.history.pushState('Access Token', null, '/')
}
return await fetch(`https://api.spotify.com/v1/search?type=track&q=${term}`, {
headers: {Authorization: `Bearer ${accessToken}`}
})
.then(res => {
if (res.ok)
return res.json();
}, networkErr => console.log(networkErr.message)
)
.then(jsonResponse => {
if (jsonResponse && jsonResponse.tracks) {
console.log(jsonResponse, 'here');
}
});
}
);
export const resultSlice = createSlice({
name: 'result',
initialState: { accessToken: '', searchResult: [] },
reducers: {
setAccessToken: (state, action) => {
state.accessToken = action.payload;
console.log('here we go', action.payload);
}
},
extraReducers(builder) {
builder
.addCase(searchTerm.fulfilled, (state, action) => {
console.log(action.payload);
})
.addCase(searchTerm.rejected, (state, action) => {
})
}
});
const { setAccessToken } = resultSlice.actions;
First, I'm checking if accessToken exists or not in state.accessToken if not, I'm redirecting user to spotify page to authorization and I'm taking accessToken from location.href and I'm trying to save tempToken to reducer but after dispatching it, it is gone and it is happening again and again. It's re-rendering, can I find any other way to prevent it to not happen?
I am using this custom http hook inside different components.
function useHttp(requestFunction, startWithPending = false) {
const httpState = useSelector((state) => state.http);
const dispatch = useDispatch();
const sendRequest = useCallback(
async function (requestData) {
dispatch(httpActions.loading(startWithPending));
try {
const responseData = await requestFunction(requestData);
dispatch(httpActions.success(responseData));
} catch (error) {
dispatch(httpActions.error(error.message || "Something went wrong!!!"));
}
},
[dispatch, requestFunction, startWithPending]
);
return {
sendRequest,
...httpState,
};
}
And I am managing states using Redux toolkit (my http actions).
const initialHttpState = {
status: null,
data: null,
error: null,
};
const httpSlice = createSlice({
name: 'http',
initialState: initialHttpState,
reducers: {
loading(state, action){
state.status = action.payload ? 'pending' : 'null';
state.data = null;
state.error = null;
},
success(state, action){
state.status = 'completed';
state.data = action.payload;
state.error = 'null'
},
error(state, action){
state.status = 'error';
state.data = null;
state.error = action.payload;
}
}
})
And I am using this hook inside useEffect's of multiple components:
const { sendRequest, data, status, error } = useHttp(getProducts, true);
useEffect(() => {
sendRequest();
}, [sendRequest]);
My problem is since different components use the same hook, whenever I tried to call this custom hook inside a component, any other component who uses this same hook get's re-rendered and it causes my app to crash (can't read properties of undefined error) because as you see I'm overwriting response data to the same place and also status are changing.
How can I avoid this re-render problem? Inside my component how can I check whether this re-render demand comes from other components or the component itself? Or how can I change my state management structure to handle this problem?
This is one of those situations where using a single global state isn't exactly what you want from a hook. Each useHttp hook should have its own internal state for managing the requests and offload the responsibility for what to do with any response values. It is a fairly trivial conversion from Redux code to using the useReducer hook so each useHttp hook has its own state and reducer logic.
Example:
const initialHttpState = {
status: null,
data: null,
error: null,
};
const reducer = (state, action) => {
switch(action.type) {
case "loading":
return {
status: action.payload ? 'pending': null,
data: null,
error: null,
};
case "success":
return {
...state,
status: 'completed',
data: action.payload,
error: null,
};
case "error":
return {
...state,
status: 'error',
data: null,
error: action.payload,
};
default:
return state;
}
};
...
const useHttp = (requestFunction, startWithPending = false) => {
const [httpState, dispatch] = React.useReducer(reducer, initialHttpState);
const sendRequest = useCallback(
async function (requestData) {
dispatch(httpActions.loading(startWithPending));
try {
const responseData = await requestFunction(requestData);
dispatch(httpActions.success(responseData));
} catch (error) {
dispatch(httpActions.error(error.message || "Something went wrong!!!"));
}
},
[dispatch, requestFunction, startWithPending]
);
return {
sendRequest,
...httpState,
};
}
you can use a key for each request and send it in the payload to update a specific key in the slice
here is an example:
const initialHttpState = {
status: null,
data: null,
error: null,
};
const httpSlice = createSlice({
name: 'http',
initialState: {},
reducers: {
loading(state, action){
if(!state[action.payload.key]) state[action.payload.key] = initialHttpState;
state[action.payload.key].status = action.payload.startWithPending ? 'pending' : 'null';
state[action.payload.key].data = null;
state[action.payload.key].error = null;
},
success(state, action){
state[action.payload.key].status = 'completed';
state[action.payload.key].data = action.payload.responseData;
state[action.payload.key].error = 'null'
},
error(state, action){
state[action.payload.key].status = 'error';
state[action.payload.key].data = null;
state[action.payload.key].error = action.payload.error;
}
}
})
in the hook you will pass the key:
function useHttp(requestFunction, startWithPending = false, key) {
const httpState = useSelector((state) => state.http[key]);
const dispatch = useDispatch();
const sendRequest = useCallback(
async function (requestData) {
dispatch(httpActions.loading({key, startWithPending}));
try {
const responseData = await requestFunction(requestData);
dispatch(httpActions.success({key, responseData}));
} catch (error) {
dispatch(httpActions.error({key, error: error.message || "Something went wrong!!!"}));
}
},
[dispatch, requestFunction, startWithPending]
);
return {
sendRequest,
...httpState,
};
}
therefore when using this hook you will add a key:
const { sendRequest, data, status, error } = useHttp(getProducts, true, 'getProducts');
useEffect(() => {
sendRequest();
}, [sendRequest]);
now your http state should look like this:
{
getProducts: { //http state here },
getUsers: { // http state here }
...
}
and because the updates in state targets a known key and not the entire state therefore it should not rerender your component
PS: : this is an anwser the question asked, but powerful libraries that do the same thing with extra functionalities and performance already exist like rtk query and react-query
I have nextJS application with Laravel backend, and I am trying to use Laravel-NextJS
All login and backend functions work fine.
Here is some login
export const useAuthLaravel = ({ middleware } = {}) => {
const router = useRouter()
const { data: user, error, mutate } = useSWR('/api/user', () =>
axios
.get('/api/user')
.then(res => res.data)
.catch(error => {
if (error.response.status != 409) throw error
router.push('/verify-email')
}),
)
const csrf = () => axios.get('/sanctum/csrf-cookie')
const login_laravel = async ({ setErrors, setStatus, ...props }) => {
await csrf()
setErrors([])
setStatus(null)
axios
.post('/login', props)
.then(() => mutate())
.catch(error => {
if (error.response.status != 422) throw error
setErrors(Object.values(error.response.data.errors).flat())
})
}
useEffect(() => {
if (middleware == 'guest' && user) return user
if (middleware == 'auth' && error && !user) logout_laravel()
}, [user, error])
return {
user,
csrf,
login_laravel,
}
}
Now above returned values of user I am trying to put in AuthContext,
import { createContext, useEffect, useReducer, useState } from 'react';
import { authApi } from 'src/mocks/auth';
const initialState = {
isAuthenticated: false,
isInitialized: false,
user: null
};
export const AuthProvider = (props) => {
const { login_laravel, logout_laravel, user } = useAuthLaravel()
const [state, dispatch] = useReducer(reducer, initialAuthState);
useEffect(() => {
const initialize = async () => {
try {
if (user) {
console.log('user exist')
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: true,
user
}
});
} else {
console.log('user not exist')
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
user: null
}
});
}
} catch (err) {
console.error(err);
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
user: null
}
});
}
};
initialize();
}, []);
const login = async (email, password) => {
try {
const user = await login_laravel({ email, password, setErrors, setStatus });
dispatch({
type: 'LOGIN',
isAuthenticated: true,
payload: {
user
}
});
} catch (err) {
dispatch({
type: 'LOGIN',
payload: {
isAuthenticated: false,
user: null
}
});
}
};
return (
<AuthContext.Provider
value={{
...state,
method: 'LARAVEL',
login,
}}
>
{children}
</AuthContext.Provider>
);
};
Now, when login function is run, it runs SWR validations, but when login finished, it does not return muted user back to AuthProvider function,
Does useAuthLaravel will run again after mutate and login function?
Thank you,
Not sure what sequence is getting wrong, any hints?
As I understand, you are trying to validate your user in UseEffect based on data from useSWR. These are "similar". My opinion is you need to modify return function for your page and not try to use validation in useEffect. (useEffect and useSWR triggers on page load, useEffect can be loaded faster than useSWR)
How I am doing this:
if (data){
//trigger a function to validate your data
return(
<>
<div>{something}</div>
</>
)
}
if (error){
//trigger a function to validate your data
return (
<div>
</div>
)
}
I am not sure it's what you are actually looking for. But I suggest you to do all data manipulations on the backend side or in getServerSideProps and not to load huge functions at the front end side. In perfect world you need to send fully fetched data to your front end.
Actions
import { FETCH_BLOG, FETCH_BLOG_ERROR, FETCH_BLOG_LOADING } from "../constants/blogActionTypes"
const initialState = {
blogs: [],
error: '',
loading: false,
allBlogs: []
}
// eslint-disable-next-line import/no-anonymous-default-export
export default (blogs = initialState, action) => {
switch (action.type) {
case FETCH_BLOG_LOADING:
return {
blogs: [...blogs.blogs],
loading: true,
error: ''
};
case FETCH_BLOG_ERROR:
return {
blogs: [...blogs.blogs],
loading: false,
error: action.payload
};
case FETCH_BLOG:
return {
blogs: [...action.payload, ...blogs.blogs],
loading: false,
error: ''
};
default: return blogs;
}
}
Reducers
export const fetchBlogs = (data) => async (dispatch) =>{
dispatch({ type: FETCH_BLOG_LOADING, payload: true })
fetch('http://localhost:5000/blog?show=' + data, {
method: 'GET',
headers: {
authorization: userData.token
}
})
.then(res => res.json())
.then(data => {
if (data.message) {
dispatch(fetchBlogsError(data.message))
} else {
dispatch({ type: FETCH_BLOG, payload: data })
}
})
}
React
const [fetchData, setFetchData] = useState(0);
const showData = () => {
setFetchData(fetchData + 10)
}
const dispatch = useDispatch();
const { loading, error, blogs, } = useSelector(state => state.blogs)
const getData = useCallback( () => {
dispatch(fetchBlogs(fetchData))
}, [fetchData])
useEffect(() => {
getData()
}, [getData])
On the first render, I fetch 10 items.after clicking on load more I fetch another 10 data from database. On the blog component it's fine but after go back to the home page and get back to the blog page; the blog items duplicates. How to fix this duplicate issue>
There are two issues here which are inter-related, you possibly don't need to address #2 depending on how you address #1.
You should add a condition to your thunk action so that you don't fetch a page that you have previously fetched.
You should separate your blog items by page so that you aren't always appending the newest items at the end of the array if you fetch page 1 twice.
Sidenote: [...blogs.blogs] is unnecessary because there is reason to clone properties which you aren't changing.
I'm confused by your API calls. It looks like /blog?show=20 is getting posts 21-30 but I would think based on the name show that it would be posts 1-20.
Using position indexes:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchBlogs = createAsyncThunk(
"blogs/fetchBlog",
async (startIndex, { getState, rejectWithValue }) => {
const res = await fetch("http://localhost:5000/blog?show=" + startIndex, {
method: "GET",
headers: {
// where does userData come from ??
authorization: userData.token
}
});
const data = await res.json();
if (data.message) {
rejectWithValue(data.message);
} else {
return data;
}
},
{
condition: (startIndex, { getState }) => {
const { blogs } = getState();
// cancel if loading of if first post on paage is loaded
if (blogs.loading || blogs.blogs[startIndex]) {
return false;
}
}
}
);
const initialState = {
blogs: [],
error: "",
loading: false
};
export default createReducer(initialState, (builder) =>
builder
.addCase(fetchBlogs.pending, (state) => {
state.loading = true;
state.error = "";
})
.addCase(fetchBlogs.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? action.error;
})
.addCase(fetchBlogs.fulfilled, (state, action) => {
const startIndex = action.meta.arg;
const newBlogs = action.payload;
// insert in the array at the correct position
state.blogs.splice(startIndex, newBlogs.length, newBlogs);
})
);
Using separated pages:
import { createAsyncThunk, createReducer, createSelector } from "#reduxjs/toolkit";
export const fetchBlogs = createAsyncThunk(
"blogs/fetchBlog",
async (pageNumber, { getState, rejectWithValue }) => {
const startIndex = 10 * (pageNumber - 1);
const res = await fetch("http://localhost:5000/blog?show=" + startIndex, {
method: "GET",
headers: {
// where does userData come from ??
authorization: userData.token
}
});
const data = await res.json();
if (data.message) {
rejectWithValue(data.message);
} else {
return data;
}
},
{
condition: (pageNumber, { getState }) => {
const { blogs } = getState();
// cancel if loading of if there is a property for this page
if (blogs.loading || blogs.blogs[pageNumber]) {
return false;
}
}
}
);
const initialState = {
//arrays keyed by page number
blogs: {},
error: "",
loading: false
};
export default createReducer(initialState, (builder) =>
builder
.addCase(fetchBlogs.pending, (state) => {
state.loading = true;
state.error = "";
})
.addCase(fetchBlogs.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? action.error;
})
.addCase(fetchBlogs.fulfilled, (state, action) => {
const pageNumber = action.meta.arg;
state.blogs[pageNumber] = action.payload;
})
);
// want to flatten the blogs array when selecting
// create a memoized selector
export const selectBlogs = createSelector(
state => state.blogs,
(blogsState) => ({
...blogsState,
blogs: Object.values(blogsState.blogs).flat(1)
})
)
With component:
export default () => {
const [pageNumber, setPageNumber] = useState(1);
const showNext = () => {
setPageNumber((page) => page + 1);
};
const dispatch = useDispatch();
const { loading, error, blogs } = useSelector(selectBlogs);
useEffect(() => {
dispatch(fetchBlogs(pageNumber));
}, [dispatch, pageNumber]);
I have a list of objects ("Albums" in my case) fetched from the database. I need to edit these objects.
In the editing component in the useEffect hook I fire up the action for getting the needed album using it's ID. This action works. However in the same useEffect I am trying to fetch the changed by before fired action redux state. And now I face the problem - all I am fetching is the previos state.
How can I implement in the useEffect fetching of current redux state?
I've seen similar questions here, however none of the answers were helpfull for my use case.
I am using redux-thunk.
Editing component. The problem appears in setFormData - it's fetching previous state from the reducer, not the current one. It seems that it fires before the state gets changed by the getAlbumById:
//imports
const EditAlbum = ({
album: { album, loading},
createAlbum,
getAlbumById,
history,
match
}) => {
const [formData, setFormData] = useState({
albumID: null,
albumName: ''
});
useEffect(() => {
getAlbumById(match.params.id);
setFormData({
albumID: loading || !album.albumID ? '' : album.albumID,
albumName: loading || !album.albumName ? '' : album.albumName
});
}, [getAlbumById, loading]);
const { albumName, albumID } = formData;
const onChange = e =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = e => {
e.preventDefault();
createAlbum(formData, history, true);
};
return ( //code );
};
EditAlbum.propTypes = {
createAlbum: PropTypes.func.isRequired,
getAlbumById: PropTypes.func.isRequired,
album: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
album: state.album
});
export default connect(
mapStateToProps,
{ createAlbum, getAlbumById }
)(withRouter(EditAlbum));
Action:
export const getAlbumById = albumID => async dispatch => {
try {
const res = await axios.get(`/api/album/${albumID}`);
dispatch({
type: GET_ALBUM,
payload: res.data
});
} catch (err) {
dispatch({
type: ALBUMS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
reducer
const initialState = {
album: null,
albums: [],
loading: true,
error: {}
};
const album = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_ALBUM:
return {
...state,
album: payload,
loading: false
};
case ALBUMS_ERROR:
return {
...state,
error: payload,
loading: false
};
default:
return state;
}
};
Will be grateful for any help/ideas
You should split up your effects in 2, one to load album when album id changes from route:
const [formData, setFormData] = useState({
albumID: match.params.id,
albumName: '',
});
const { albumName, albumID } = formData;
// Only get album by id when id changed
useEffect(() => {
getAlbumById(albumID);
}, [albumID, getAlbumById]);
And one when data has arrived to set the formData state:
// Custom hook to check if component is mounted
// This needs to be imported in your component
// https://github.com/jmlweb/isMounted
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
// In your component check if it's mounted
// ...because you cannot set state on unmounted component
const isMounted = useIsMounted();
useEffect(() => {
// Only if loading is false and still mounted
if (loading === false && isMounted.current) {
const { albumID, albumName } = album;
setFormData({
albumID,
albumName,
});
}
}, [album, isMounted, loading]);
Your action should set loading to true when it starts getting an album:
export const getAlbumById = albumID => async dispatch => {
try {
// Here you should dispatch an action that would
// set loading to true
// dispatch({type:'LOAD_ALBUM'})
const res = await axios.get(`/api/album/${albumID}`);
dispatch({
type: GET_ALBUM,
payload: res.data
});
} catch (err) {
dispatch({
type: ALBUMS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
Update detecting why useEffect is called when it should not:
Could you update the question with the output of this?
//only get album by id when id changed
useEffect(() => {
console.log('In the get data effect');
getAlbumById(albumID);
return () => {
console.log('Clean up get data effect');
if (albumID !== pref.current.albumID) {
console.log(
'XXXX album ID changed:',
pref.current.albumID,
albumID
);
}
if (getAlbumById !== pref.current.getAlbumById) {
console.log(
'XXX getAlbumById changed',
pref.current.getAlbumById,
getAlbumById
);
}
};
}, [albumID, getAlbumById]);