react Login with useContext useReducer basic example - reactjs

I'm using Typescript. I want to
Create a context
Use the context in routing
Update the context when logged in.
I'm following this tutorial, except Typescript cut my hopes short. See below
I have this in my App.tsx
import React from 'react';
import './App.css';
import Login from "./auth/login";
import Home from "./dash/home";
import Header from "./menu";
const initialState = {
isAuthenticated: false,
user: null,
token: null,
};
export const AuthContext = React.createContext(); // <---- This right here complains:
// Expected 1-2 arguments, but got 0.ts(2554)
// index.d.ts(349, 9): An argument for 'defaultValue' was not provided.
const reducer = (state: any, action: any) => {
switch (action.type) {
case "LOGIN":
return {
...state,
isAuthenticated: true,
user: action.payload.user,
token: action.payload.token
};
case "LOGOUT":
localStorage.clear();
return {
...state,
isAuthenticated: false,
user: null
};
default:
return state;
}
};
const App: React.FC = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<AuthContext.Provider
value={{
state,
dispatch
}}
>
<Header />
<div className="App">{!state.isAuthenticated ? <Login /> : <Home />}</div>
</AuthContext.Provider>
);
}
export default App;
In my login page, I have this:
import React from "react";
import { useForm } from 'react-hook-form';
import axios from 'axios';
import { AuthContext } from "../App";
export const Login = () => {
const { dispatch } = React.useContext(AuthContext) // <--- This the right way to do it?
// Where is the "dispatch" magically coming from?
const onSubmit = (data: any) => {
axios.post('http://localhost/api/v1/users/login/', data)
.then(res => {
console.log(res);
})
}
return (
<div className="login-container">
<!-- assume there's a form here -->
</div>
);
};
export default Login;
So
What do I put in for the "defaultValue"?
How do I update the context after login?
EDIT:
More context (pun intended) of what I wanna achieve.
The App.tsx has an <AuthContext.Provider ...> ... </AuthContext.Provider> If I understand correctly, this Context takes in the value of the state, dispatch and as per the {!state.isAuthenticated ... } part, dynamically alternates between <Login /> and <Home/> component.
This <AuthContext.Provider/> as per the initialState is set to isAuthenticated: false
When the user logs in at the Login Component, my question is, how do I, via the AuthContext, update the values in the App.tsx so as the routing will change?

If you take a look at the docs about React.createContext
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This can be helpful for testing components in isolation without wrapping them. Note: passing undefined as a Provider value does not cause consuming components to use defaultValue.
So you don't need to pass anything to the context, but you are getting the error because of typescript.
You need to create an interface for the context value.
You are passing the context as
value={{
state,
dispatch
}}
But you need to have an interface for that when create the context.
export const AuthContext = React.createContext(); // <---- missing interface
Here is an example on how to do it.
Where is the "dispatch" magically coming from?
This is how useContext works.
The value you get form useContext(MyContex) is the value passed by MyContext.Provider by value prop.
e.g.
<MyContext.Provider value={fooValue}>
{/* ... */}
</MyContext.Provider>
// same fooValue from the Provider
const fooValue = useState(MyContext)
Edit
After the async call inside onSubmit you must call dispatch and pass {type: 'LOGIN', payload: DATA_FROM_API} so it does in the reducer method and sets isAuthenticated to true
const onSubmit = (data: any) => {
axios.post('http://localhost/api/v1/users/login/', data)
.then(res => {
console.log(res.data) // <= if this is `payload`, than pass it to dispatch
dispatch({type: 'LOGIN', payload: res.data})
})
}

I had a similar problem, but after many trials and errors I was able to fix it.
I was following this tutorial:
Link: https://soshace.com/react-user-login-authentication-using-usecontext-and-usereducer/
Code: https://github.com/nero2009/login-auth-useContext
The problem about routing is also covered there. But it is in JS, so you have to make some modifications. Here is what I did:
First, I specified the action types, in your example it would be:
export type AuthenticationAction = { type: 'LOGIN', payload: IPayload } | {type: 'LOGOUT'}
I specified an interface for my payload, but it may work with "any" as well.
Then, I specified the default values (this is probably what you were looking for):
const AuthenticationStateContext = React.createContext(initialState)
const AuthenticationDispatchContext = React.createContext({} as Dispatch<AuthenticationAction>)
After that it should work.

Related

React Context value never updates

I have the following code as a component that returns a context. For some reason when I call the updateUser function it is not setting the state, I keep getting a blank object. Is there something different I have to do to a function like that when it has parameters?
import React, { useState } from "react";
const UserContext = React.createContext({
user: {},
updateUser: (incomingUser) => {},
});
const UserData = (props) => {
const [user, setUser] = useState({});
const updateUser = (incomingUser) => {
setUser(incomingUser);
console.log(`user=${JSON.stringify(incomingUser)}`);
};
return (
<UserContext.Provider value={{ user, updateUser }}>
{props.children}
</UserContext.Provider>
);
};
export { UserData, UserContext };
Get rid of the default value for UserContext, or initialise it as null:
const UserContext = React.createContext(null);
Per the React Docs:
When React renders a component that subscribes to this Context object it will read the current context value from the closest matching Provider above it in the tree.
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This default value can be helpful for testing components in isolation without wrapping them.

Jest with Enzyme, why is `toHaveBeenCalled` not triggering in my React Component test?

I'm working on a unit test in a React application that verifies a passed in prop function is being conditionally called based on another props value. I'm utilizing Typescript/Enzyme/Jest in my application and am using a Root wrapper around the component I'm testing to inject the Redux store (and override initial state if desired).
import { mount, ReactWrapper } from "enzyme";
import React from "react";
import Login from "src/components/auth/Login";
import Root from "src/Root";
let wrapped: ReactWrapper;
let defaultProps = {
signIn: jest.fn(),
token: null,
};
beforeEach(() => {
wrapped = mount(
<Root>
<Login {...defaultProps} />
</Root>
);
});
describe("on mount", () => {
describe("if no token is supplied to the props", () => {
it("will call the props.signIn() function", () => {
expect(defaultProps.signIn).toHaveBeenCalled();
});
});
});
When I run the test the toHaveBeenCalled() (as well as toBeCalled(), tried both) are not registering any calls. However, I have supplied a console.log statement that is getting triggered within the same conditional that the signIn() function lives.
import React from 'react';
import { AuthState, JWT } from "src/components/auth/types";
import { signIn } from "src/redux/auth";
interface Props {
signIn: () => Promise<void>;
token: null | JWT;
}
class Login extends React.Component<Props> {
/**
* Sign the user in on mount
*/
public componentDidMount(): void {
if (!this.props.token) {
console.log("GETTING HERE");
this.props.signIn();
}
}
public render(): JSX.Elemeent {
// ... More code
}
}
const mapStateToProps = (state: AuthState) => {
return {
token: state.auth.token;
};
};
export default connect(mapStateToProps, { signIn })(Login);
I've gone through several related posts/articles but all of the different configurations, such as traversing enzyme to get the direct prop or utilizing spyOn, have failed.
The only thing I can figure that's different is my wrapping of the Login component with Root, but considering I can see the console.log being triggered this seems like a complete shot in the dark.
Can anyone tell me what I'm doing wrong here?
You've to wait for component to mount, so:
it("will call the props.signIn() function", (done) => {
setImmediate(() => {
expect(defaultProps.signIn).toHaveBeenCalled();
done()
});
});
Ended up being me forgetting to place the override via mapDispatchToProps and mapStateToProps in the connect function. This was causing my passed in signIn function to be overridden by the signIn action imported in the file. Updating with ownProps and conditionally utilizing the passed in value fixes the issue:
const mapStateToProps = (state: AuthState, ownProps: Props) => {
return {
token: ownProps.token || state.auth.token;
};
};
const mapDispatchToProps = (dispatch: ThunkDispatch<{}, {}, any>, ownProps: Props) => {
return {
signIn: ownProps.signIn || (() => { return dispatch(signIn()) })
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);

I am using React Context and would like to get confirmed my structure

It is my first application using react context with hooks instead of react-redux and would like to get help of the structure of the application.
(I'm NOT using react-redux or redux-saga libraries.)
Context
const AppContext = createContext({
client,
user,
});
One of actions example
export const userActions = (state, dispatch) => {
function getUsers() {
dispatch({ type: types.GET_USERS });
axios
.get("api address")
.then(function(response) {
dispatch({ type: types.GOT_USERS, payload: response.data });
})
.catch(function(error) {
// handle error
});
}
return {
getUsers,
};
};
Reducer (index.js): I used combineReducer function code from the redux library
const AppReducer = combineReducers({
client: clientReducer,
user: userReducer,
});
Root.js
import React, { useContext, useReducer } from "react";
import AppContext from "./context";
import AppReducer from "./reducers";
import { clientActions } from "./actions/clientActions";
import { userActions } from "./actions/userActions";
import App from "./App";
const Root = () => {
const initialState = useContext(AppContext);
const [state, dispatch] = useReducer(AppReducer, initialState);
const clientDispatch = clientActions(state, dispatch);
const userDispatch = userActions(state, dispatch);
return (
<AppContext.Provider
value={{
clientState: state.client,
userState: state.user,
clientDispatch,
userDispatch,
}}
>
<App />
</AppContext.Provider>
);
};
export default Root;
So, whenever the component wants to access the context store or dispatch an action, this is how I do from the component :
import React, { useContext } from "react";
import ListMenu from "../common/ListMenu";
import List from "./List";
import AppContext from "../../context";
import Frame from "../common/Frame";
const Example = props => {
const { match, history } = props;
const { userState, userDispatch } = useContext(AppContext);
// Push to user detail route /user/userId
const selectUserList = userId => {
history.push(`/user/${userId}`);
userDispatch.clearTabValue(true);
};
return (
<Frame>
<ListMenu
dataList={userState.users}
selectDataList={selectUserList}
/>
<List />
</Frame>
);
};
export default Example;
The problem I faced now is that whenever I dispatch an action or try to access to the context store, the all components are re-rendered since the context provider is wrapping entire app.
I was wondering how to fix this entire re-rendering issue (if it is possible to still use my action/reducer folder structure).
Also, I'm fetching data from the action, but I would like to separate this from the action file as well like how we do on redux-saga structure. I was wondering if anybody know how to separate this without using redux/redux-saga.
Thanks and please let me know if you need any code/file to check.
I once had this re-rendering issue and I found this info on the official website:
https://reactjs.org/docs/context.html#caveats
May it will help you too
This effect (updating components on context update) is described in official documentation.
A component calling useContext will always re-render when the context value changes. If re-rendering the component is expensive, you can optimize it by using memoization
Possible solutions to this also described
I see universal solution is to useMemo
For example
const Example = props => {
const { match, history } = props;
const { userState, userDispatch } = useContext(AppContext);
// Push to user detail route /user/userId
const selectUserList = userId => {
history.push(`/user/${userId}`);
userDispatch.clearTabValue(true);
};
const users = userState.users;
return useMemo(() => {
return <Frame>
<ListMenu
dataList={users}
selectDataList={selectUserList}
/>
<List />
</Frame>
}, [users, selectUserList]); // Here are variables that component depends on
};
I also may recommend you to completly switch to Redux. You're almost there with using combineReducers and dispatch. React-redux now exposes useDispatch and useSelector hooks, so you can make your code very close to what you're doing now (replace useContext with useSelector and useReducer with useDispatch. It will require minor changes to arguments)

How to properly ask for props on component

I have a component SinglePost that is called when another component Post is clicked in the main page, and also through path /post/:id. The thing is, in SinglePost, I call an API through Redux on componentDidMount, and then ask for it on componentWillReceiveProps. And when you access the component from the URL /post/:id, the props throws undefined two times after it loads.
I was following a React/Redux tutorial, to make a CRUD web app with API calls. And, in that project, the props loads fine in both cases (through main page navigation, and from URL). I did exactly the same thing and it doesn't work. Is this a problem, or does it work this way?
This is the SinglePost component:
import { connect } from 'react-redux';
import { mostrarPublicacion } from '../../actions/publicacionesActions';
state = {
publicacion: {},
}
componentDidMount() {
const {id} = this.props.match.params;
//this is the action defined on PublicacionesActions
this.props.mostrarPublicacion(id);
}
componentWillReceiveProps(nextProps, nextState) {
const {publicacion} = nextProps;
this.setState({
publicacion
})
}
const mapStateToProps = state => ({
publicacion: state.publicaciones.publicacion
})
export default connect(mapStateToProps, {mostrarPublicacion}) (SinglePost);
PublicacionesActions:
export const mostrarPublicacion = (id) => async dispatch => {
const respuesta = await axios.get(`http://www.someapi.com:8000/api/publicacion/${id}`);
dispatch({
type: MOSTRAR_PUBLICACION,
payload: respuesta.data
})
}
I debugged this and it actually returns the publication. In fact, it renders properly in the SinglePost component. But at first it loads undefined when accessing the component through the url.
RootReducer:
import { combineReducers } from 'redux';
import publicacionesReducer from './publicacionesReducer';
import seccionesReducer from './seccionesReducer';
export default combineReducers({
publicaciones: publicacionesReducer,
secciones: seccionesReducer
});
PublicacionesReducer:
export default function (state = initialState, action) {
switch(action.type) {
case MOSTRAR_PUBLICACION:
return {
...state,
publicacion: action.payload
}
default:
return state
}
}
This is the Router component (wrapped into <Provider store={store}>):
<Switch>
<Route exact path='/post/:id' component={SinglePost} />
</Switch>
I actually ask for (!this.state.publication) and return null, to render the component. It is working, but it is necesary?

Testing Redux Enzyme: Component doesn't re-render after state has changed

I have a connected component (to Redux store) called DrawerAvatar, that I export for testing purpose (Enzyme + Jest) both the connected and non connected version.
Basically, I want to test that my DrawerAvatar render the user avatar when my Redux state isAuthenticated is true, and it renders a logo picture when isAuthenticated is false.
DrawerAvatar.js
export class DrawerAvatar extends React.Component {
render () {
const avatarSrc = this.props.isAuthenticated ?
'http://user-avatar.png'
) : (
'http://logo.png'
);
return (
<StyledAvatarContainer>
<StyledAvatar src={avatarSrc} />
</StyledAvatarContainer>
);
}
}
const mapStateToProps = state => ({
isAuthenticated: state.authReducer.isAuthenticated
});
export default compose(
connect(mapStateToProps, null)
)(DrawerAvatar);
And in my test, I'm using the non-connected DrawerAvatar, and connect it to my real Redux store via the Provider like this: (initiale state: isAuthenticated: false)
DrawerAvatar.test.js:
import React from 'react';
import { shallow } from 'enzyme';
import { Provider } from 'react-redux';
import store from '../../store';
import connectedDrawerAvatar, { DrawerAvatar } from './DrawerAvatar';
describe('Header > DrawerAvatar: component', () => {
it('should render logo for the DrawerAvatar if not authenticated, and the user avatar if authenticated', () => {
const wrapper = shallow(<Provider store={store}><DrawerAvatar /></Provider>);
console.log(wrapper.dive().debug());
// Output:
// <StyledAvatarContainer>
// <StyledAvatar src="https://logo.png" />
// </StyledAvatarContainer>
const StyledAvatarSrc = wrapper.dive().find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://logo.png'); // assertion passed
store.dispatch({ type: 'LOGIN_WITH_EMAIL_REQUESTED_TEST' });
// the state has been correctly updated, now isAuthenticated: true
console.log(wrapper.dive().debug());
// Output: same as above, whereas it should be:
// <StyledAvatarContainer>
// <StyledAvatar src="https://user-avatar.png" />
// </StyledAvatarContainer>
expect(StyledAvatarSrc).toBe('https://user-avatar.png'); // assertion failed
});
});
And here is my authReducer:
authReducer.js
const initialState = {
isAuthenticated: false
};
export default function authReducer (state = initialState, action) {
switch (action.type) {
case 'LOGIN_WITH_EMAIL_REQUESTED_TEST':
return {
...state,
isAuthenticated: true,
};
default:
return state;
}
}
So basically, I have a real action with type of LOGIN_WITH_EMAIL_REQUESTED that will call a bunch of Redux-saga with Axios, but for testing purpose, I just added to my real authReducer a LOGIN_WITH_EMAIL_REQUESTED_TEST case that will set the state isAuthenticated to true to avoid Axios calls etc...Not sure if it's a good way to do things though..lol
I've tried in vain to force the component to update with wrapper.update()...
I've also looked at redux-mock-store, but it seems like you cannot modify the state and only deals with actions and not the states.
I just start writing my first React test so...thank you !
Basically, I want to test that my DrawerAvatar render the user avatar when my Redux state isAuthenticated is true, and it renders a logo picture when isAuthenticated is false.
I would recommend not connecting the entire connected component and trying to bother with Redux at all. You can achieve your expected result with the following:
describe('Header > DrawerAvatar: component', () => {
it('should render logo for the DrawerAvatar if not authenticated, and the user avatar if authenticated', () => {
let wrapper = shallow(<DrawerAvatar isAuthenticated={false} />);
let StyledAvatarSrc = wrapper.find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://logo.png');
wrapper = shallow(<DrawerAvatar isAuthenticated={true} />);
StyledAvatarSrc = wrapper.find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://user-avatar.png');
});
});
Then, you can write separate tests for each of the pieces involved, for example your mapStateToProps function, which is really just a simple function that returns an object based on its input. And another simple test of your authReducer, etc

Resources