I am trying to show information of a state into a component. I am using the context to load information from different origin.
const router = useRouter()
const [sidebarOpen, setSidebarOpen] = useState(false)
const [loadUser, setLoadUser] = useState({})
const { state, dispatch } = useContext(Context)
const { user } = state
useEffect(() => {
if (user == null) {
router.push("/auth/login")
}
setLoadUser(user)
}, [user])
That code is inside a dashboard component. The idea is to get the user information into the state to show it on the dashboard. The problem is that the useEffect is executed at the same time that the content is rendered, therefore it does not have time to load the information, and the variables are null for me.
Here is an image of how the loadState variable behaves once inside the render.
I am using nextjs by the way.
I am passing the context with a provider to the App. And wrapped it.
// initial state
const initialState = {
user: null
};
// Create context
const Context = createContext()
// root reducer
const reducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case "LOGIN":
return { ...state, user: payload};
case "LOGOUT":
return { ...state, user: null };
default:
return state;
}
}
// context provider
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const router = useRouter();
useEffect(() => {
dispatch({
type: "LOGIN",
payload: JSON.parse(window.localStorage.getItem("user"))
})
}, [])
axios.interceptors.response.use(
function (response) {
return response
},
function (error) {
let res = error.response;
console.log(error)
if (res.data.status === 401 && res.config && !res.config.__isRetryRequest) {
return new Promise((resolve, reject) => {
axios.get(`${process.env.NEXT_PUBLIC_API}/auth/logout`)
.then(res => {
dispatch({ type: "LOGOUT" })
window.localStorage.removeItem("user")
router.push('/auth/login')
})
.catch(error => {
reject(error)
})
})
}
return Promise.reject(error);
}
)
useEffect(() => {
const getCsrfToken = async () => {
const { data } = await axios.get(`${process.env.NEXT_PUBLIC_API}/csrf-token`)
axios.defaults.headers["X-CSRF-Token"] = data.csrfToken
}
getCsrfToken()
}, [])
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
)
}
export { Context, Provider }
Add dependecy array. That shoud be quick fix.
useEffect(() => {
if (user == null) {
router.push("/auth/login")
}
setLoadUser(user)
}, [state]) // dependecy array looks at state changes
You can read more about dependecy array on the official react documentation: https://reactjs.org/docs/hooks-effect.html
proper fix:
whenever you are using api calls in your app, those are asyncrhonus. You should use some loading state.
for example
const Context = () => {
const [loading, setLoading] = useState(true)
const [user, setUser] = useState(true)
fetch('api/user').than(r=> setLoading(false);setUser(r.user))
// rest of the context code
}
and in component itself:
const state = useContext(Context)
useEffect(() => {
state.loading ? null : setLoadUser(user)
}, [state]) //
Related
The url I put inside axios.get is a wrong url. I am expecting an error message through action.error.message in the reducer section. But it's not returning any message despite the fetch request being failed. The code as follows:
usersSlice.js
const createSlice = require("#reduxjs/toolkit").createSlice;
const createAsyncThunk = require("#reduxjs/toolkit").createAsyncThunk;
const axios = require("axios");
const initialState = {
loading: false,
users: [],
error: "",
};
const fetchUsers = createAsyncThunk("users/fetchUsers", () => {
return axios
.get("https://jsonplaceholder.typicodeasdf.com/users")
.then((res) => console.log(res.data.map((user) => user.name)));
});
const usersSlice = createSlice({
name: "users",
initialState,
extraReducers: (builder) => {
builder.addCase(fetchUsers.pending, (state) => {
state.loading = true;
});
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
console.log("success");
});
builder.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.users = [];
state.error = action.error.message;
console.log("failed");
});
},
});
module.exports = usersSlice.reducer;
module.exports.fetchUsers = fetchUsers;
store.js
const configureStore = require("#reduxjs/toolkit").configureStore;
const reduxLogger = require("redux-logger");
const usersSlice = require("./Features/users/userSlice");
const store = configureStore({
reducer: {
usersSlice: usersSlice,
},
});
module.exports = store;
index.js
const { fetchUsers } = require("./App/Features/users/userSlice.js");
const store = require("./App/store.js");
console.log("intial state", store.getState());
const unsubscribe = store.subscribe(() =>
console.log("updated state", store.getState())
);
store.dispatch(fetchUsers());
unsubscribe();
output
intial state { usersSlice: { loading: false, users: [], error: '' } }
updated state { usersSlice: { loading: true, users: [], error: '' } }
failed
Note
In the output I am expecting
updated state { usersSlice: { loading: false, users: [], error: 'some error message' } }
The thunk function in RTK has a built-in method to return an error message if the promise is rejected. I was actually willing to get that message. However, I resolved the problem. Instead of returning the data I actually console logged it. And completely forgot to fix it.
const fetchUsers = createAsyncThunk("users/fetchUsers", () => {
return axios
.get("https://jsonplaceholder.typicodeasdf.com/users")
**.then((res) => console.log(res.data.map((user) => user.name)));**
});
I am trying to learn how to effectively use React Hooks but am having an issue. I would like to reflect whether or not a user is "logged in" to the site using a JWT in local storage. When I first visit the page, the hook works as I intend, retrieving the user data. But if I click the "Log Out" button in the example below, the component does not update to reflect this, although it will if I refresh the page. How might I properly implement this hook to get it to update when logging in/out?
Custom hooks:
export const useUser = () => {
const [token] = useToken();
const getPayloadFromToken = token => {
const encodedPayload = token.split('.')[1];
return JSON.parse(atob(encodedPayload))
}
const [user,setUser] = useState(() => {
if(!token) return null;
return getPayloadFromToken(token);
})
useEffect(() => {
if(!token) {
setUser(null);
} else {
setUser(getPayloadFromToken(token));
}
}, [token]);
return user;
}
export const useToken = () => {
const [token, setTokenInternal] = useState(() => {
return localStorage.getItem('token');
});
const setToken = newToken => {
localStorage.setItem('token',newToken);
setTokenInternal(newToken);
}
return [token, setToken];
}
Navigation Bar Component:
const NavigationBar = () => {
const user = useUser();
const logOut = () => {
localStorage.removeItem('token');
};
return(
<>
<div>{user ? 'logged in' : 'logged out'}</div>
<button onClick={logout}>Log Out</button>
</>
);
}
I think at a minimum you could expose out the setToken function directly via the useUser hook and when you call logout call setToken(null) (or similar) and this would be sufficient enough to trigger a render. Ideally though you'd have all this authentication "state" centrally located in a React context so the hooks all reference the same single state.
I suggest actually encapsulating the logout function within the useUser hook and exposing that out instead of directly exposing the setToken function. You want the hooks to maintain control over the state invariant and not rely on consumers to pass/set the correct state values.
Example:
export const useUser = () => {
const [token, setToken] = useToken();
const getPayloadFromToken = token => {
const encodedPayload = token.split('.')[1];
return JSON.parse(atob(encodedPayload));
}
const [user, setUser] = useState(() => {
if (!token) return null;
return getPayloadFromToken(token);
});
useEffect(() => {
if (!token) {
setUser(null);
} else {
setUser(getPayloadFromToken(token));
}
}, [token]);
const logout = () => {
setUser(null);
setToken(null);
};
return { user, logout };
}
...
export const useToken = () => {
const [token, setTokenInternal] = useState(() => {
// Initialize from localStorage
return JSON.parse(localStorage.getItem('token'));
});
useEffect(() => {
// Persist updated state to localStorage
localStorage.setItem('token', JSON.stringify(newToken));
}, [token]);
const setToken = newToken => {
setTokenInternal(newToken);
}
return [token, setToken];
};
...
const NavigationBar = () => {
const { logout, user } = useUser();
return(
<>
<div>{user ? 'logged in' : 'logged out'}</div>
<button onClick={logout}>Log Out</button>
</>
);
}
I encountered something similar. A solution I found that worked for me was to save whether or not the user is logged in using Context. Effectively this would involve creating a wrapper around components in your app which need access to whether or not a user is logged in as an alternative to using local storage to save this sort of stuff.
https://reactjs.org/docs/context.html
I need to reset global variable on custom hook when unit testing React component. I have read few tutorials and StackOverflow answers to this simple task, but without luck to implement it correctly.
The problem
userInfo is undefined in the first and second test but when runs the third test userInfo is defined then on useEffect doesn't change the value... So my question is how to reset userInfo for each test.
jest.resetModules // doesn't work
jest.isolateModules // doesn't work
My simplest possible setup for single test is as following:
My Environment
"jest": "^24.9.0",
My Hook
import {useState, useEffect} from "react";
// This variable is an object save user info
let userInfo = null;
export default (authService) => {
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (userInfo !== null || authService === null) {
return;
}
setLoading(true);
authService
?.getUser()
.then((response) => {
userInfo = {owners: [{...response, cost_center: response.costCenter || "N/A"}]};
})
.catch(() => {
setError({
title: "authService Error",
message: "Error getting user",
status: 500
});
})
.finally(() => setLoading(false));
}, [authService]);
return [userInfo, error, loading];
};
My Test
import {renderHook} from "#testing-library/react-hooks";
import * as sinon from "sinon";
import {getSpyOfUseOktaAuth} from "../../../__tests__/";
import {Info, InfoFromRequest, InfoWithNoCostCenter} from "../../../__tests__/";
describe("useGetUserInfo", () => {
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
jest.useFakeTimers();
});
afterAll(() => {
clock.restore();
});
it("should set the error value after the getUserInfo function throws an error", async () => {
const useGetUserInfo = require("../index").default;
const errorMessage = {
title: "authService Error",
message: "Error getting user from",
status: 500
};
const getAuthMock = getSpyOfUseAuth({
Auth: {
signOut: jest.fn(),
getUser: jest.fn(async () => {
throw new Error("Auth Error");
})
},
authState: {}
});
const {result, rerender, waitForNextUpdate} = renderHook(() =>
useGetUserInfo(getAuthMock.results.Auth)
);
rerender();
await waitForNextUpdate();
expect(result.current[1]).toEqual(errorMessage);
getAuthMock.instance.mockRestore();
});
it("should return the user info from after run the getUserInfo function", async () => {
const useGetUserInfo = require("../index").default;
let authService = null;
const {result, rerender, waitForNextUpdate} = renderHook(() => useGetOktaUserInfo(authService));
const getAuthMock = getSpyOfUseAuth({
Auth: {
signOut: jest.fn(),
getUser: jest.fn(async () => Info)
},
authState: {}
});
authService = getAuthMock.results.Auth;
rerender();
await waitForNextUpdate();
expect(result.current[0]).toEqual(InfoFromRequest);
getAuthMock.instance.mockRestore();
});
it("should set cost_center as in data as N/A if costCenter is not defined in user info ", async () => {
const useGetUserInfo = require("../index").default;
const getAuthMock = getSpyOfUseAuth({
Auth: {
signOut: jest.fn(),
getUser: jest.fn(async () => InfoWithNoCostCenter)
},
authState: {}
});
const {result, rerender} = renderHook(() => useGetUserInfo(getAuthMock.results.Auth));
rerender();
expect(result.current[0].owners[0].cost_center).toEqual("N/A");
getAuthMock.instance.mockRestore();
});
});
I would say that either you export the 'userInfo' variable from the hook and you set it to null manually before each test, or you treat 'userInfo' as a state variable just like 'error' and 'loading'
If you go for the first option, you will need to export by reference Node Modules - exporting a variable versus exporting functions that reference it?
For the second option, it would be something like this
import {useState, useEffect} from "react";
export default (authService) => {
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [userInfo, setUserInfo] = useState(null);
useEffect(() => {
if (userInfo !== null || authService === null) {
return;
}
setLoading(true);
authService
?.getUser()
.then((response) => {
setUserInfo({owners: [{...response, cost_center: response.costCenter || "N/A"}]});
})
.catch(() => {
setError({
title: "authService Error",
message: "Error getting user",
status: 500
});
})
.finally(() => setLoading(false));
}, [authService]);
return [userInfo, error, loading];
};
The solution I got is to separate each case into different files. As jest load each file in a different process, this will be enough
I want to convert this class component into a function component. I tried a lot and failed. Is there a solution to the problem?
componentDidMount(){
const id = this.props.match.params.id;
getById( parseInt(id) )
.then(product => {
this.setState({
product,
loading: false
});
})
}
The following snippet is equal componentDidMount in class component:
// ...
const [loading, setLoading] = React.useState(false);
const [product, setProduct] = React.useState(void 0);
React.useEffect(() => {
// Did mount, put your code here
setLoading(true)
// Fetch done
.then(product => {
setLoading(false)
setProduct(product)
})
}, []);
So I have written a custom polling hook which uses useContext and useLazyQuery hooks. I want to write a unit test for this, which should cover its returned values state and side effect.
So far I have managed to do this much but I'm not so sure how to proceed ahead. Any tips?
export const useUploadActivityPolling = (
teId: TeIdType
): UploadActivityPollingResult => {
const { dispatch, uploadActivityId }: StoreContextType = useAppContext();
const [fetchActivityStatus, { error: UploadActivityError, data: UploadActivityData, stopPolling }] = useLazyQuery(
GET_UPLOAD_ACTIVITY,
{
pollInterval: 3000,
fetchPolicy: 'network-only',
variables: { teId, activityId: uploadActivityId },
}
);
useEffect(() => {
if (UploadActivityData) {
setUploadActivityId(
UploadActivityData.getUploadActivityStatus.activity_id,
dispatch
);
updateActivityStateAction(UploadActivityData.getExcelUploadActivityStatus.status, dispatch);
}
}, [UploadActivityData]);
return { fetchActivityStatus, stopPolling, UploadActivityError };
};
import React from 'react';
import { mount } from 'enzyme';
const TestCustomHook = ({ callback }) => {
callback();
return null;
};
export const testCustomHook = callback => {
mount(<TestCustomHook callback={callback} />);
};
describe('useUploadActivityPolling', () => {
let pollingResult;
const teId = 'some id';
beforeEach(() => {
testCustomHook(() => {
pollingResult = useUploadActivityPolling(teId);
});
});
test('should have an fetchActivityStatus function', () => {
expect(pollingResult.fetchActivityStatus).toBeInstanceOf(Function);
});
});