useSelector hooks problem after submitting data - reactjs

I'm not sure if the problem is in useSelector or in useDispatch hooks or in another place, so here is the scenario:
Two screens (HomeScreen & AddBlogScreen)
In HomeScreen I click add blog button then it redirect to AddBlogScreen
I input the data, then submit. After the submit is success then redirect to HomeScreen
As mentioned in below pic, I got the no 4 result & I have to refresh to get the no 3 result. But my expectation is no 3 pic without getting the error.
Here is my code:
HomeScreen
import jwtDecode from "jwt-decode";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router";
import { blogList } from "../redux/action";
export const MainScreen = () => {
// const [token, setToken] = useState(localStorage.getItem("token"));
const user = jwtDecode(localStorage.getItem("token"));
const history = useHistory();
const dispatch = useDispatch();
useEffect(() => {
dispatch(blogList());
}, [dispatch]);
const { blog } = useSelector((state) => state.blog);
console.log(blog);
return (
<>
<button
onClick={() => {
localStorage.removeItem("token");
history.push("/");
}}
>
singout
</button>
<button
onClick={() => {
history.push({ pathname: "/Blog", state: user });
}}
>
add blog
</button>
<h1 style={{ color: "red" }}>username: {user.username}</h1>
{blog.map(({ id, b_title, b_content, category_id }) => (
<div key={id}>
<h1
onClick={() =>
history.push({
pathname: "/Edit",
state: { id, b_title, b_content, category_id },
})
}
>
Title: {b_title}
</h1>
<p>Content: {b_content}</p>
</div>
))}
</>
);
};
AddBlogScreen
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory, useLocation } from "react-router";
import { addBlog } from "../redux/action";
export const AddBlogScreen = () => {
const history = useHistory();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [category, setCategory] = useState("");
const dispatch = useDispatch();
const location = useLocation();
const author = location.state.id;
const submitHandler = (e) => {
e.preventDefault();
dispatch(addBlog(title, content, author, category));
setTitle("");
setContent("");
setCategory("");
history.push("/Home");
};
return (
<div>
<h1>add blog page</h1>
<form onSubmit={submitHandler}>
<input
type="text"
placeholder="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<br />
<input
type="text"
placeholder="content"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<br />
<br />
<input
type="text"
placeholder="category"
value={category}
onChange={(e) => setCategory(e.target.value)}
/>
<br />
<br />
<input
type="submit"
value="submit"
disabled={
title === "" || content === "" || category === "" ? true : false
}
/>
</form>
</div>
);
};
actions
import axios from "axios";
import {
LIST_BLOG,
ADD_BLOG,
EDIT_BLOG,
DELETE_BLOG,
LOGIN_USER,
REGISTER_USER,
LOGOUT_USER,
} from "./constant";
// ==================== blog actions ======================
export const blogList = () => async (dispatch) => {
try {
const result = await axios
.get("http://localhost:3001/api/v1/blog?page=0")
.then((res) => res.data.data)
.catch((err) => err);
dispatch({
type: LIST_BLOG,
payload: result,
});
} catch (err) {
dispatch({
payload: err,
});
}
};
export const addBlog =
(title, content, author, category) => async (dispatch) => {
try {
const result = await axios
.post("http://localhost:3001/api/v1/blog", {
blog_title: title,
blog_content: content,
author_id: author,
category_id: category,
})
.then(alert("success add blog"))
.catch((err) => alert(err));
dispatch({
type: ADD_BLOG,
payload: result,
});
} catch (err) {
dispatch({
payload: err,
});
}
};
reducer
const initial_state = {
blog: [],
};
export const blogReducer = (state = initial_state, action) => {
switch (action.type) {
case LIST_BLOG:
return {
...state,
blog: action.payload,
};
case ADD_BLOG:
return {
...state,
blog: action.payload,
};
case EDIT_BLOG:
return {
...state,
blog: action.payload,
};
case DELETE_BLOG:
return {
...state,
blog: action.payload,
};
default:
return state;
}
};
store
import { blogReducer, userReducer } from "./reducer";
import { combineReducers, createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
const reducer = combineReducers({
blog: blogReducer,
user: userReducer,
});
const middleWare = composeWithDevTools(applyMiddleware(thunk));
export const store = createStore(reducer, middleWare);

First of all, the origin of error:
the error says a property named map on blog is not a function, meaning blog is not an array.
This is where it is coming from:
const { blog } = useSelector((state) => state.blog);
Your state is a an ojbect with a property named blog, you can access it these two ways:
const { blog } = useSelector((state) => state);
or
const blog = useSelector((state) => state.blog);
Other issues I noticed :
in addBlog:
1. When you are using try-catch with await, it's not a good idea to use then-catch too.
2.result won't be the blog data you expect. It will be an object, which is an instance of AxiosResponse, which includes the data.
you can extract the data from response object this way:
let response = await axios.post(... // some api request
let {data}=response
I would edit it like this:
export const addBlog =
(title, content, author, category) => async (dispatch) => {
try {
const {data} = await axios
.post("http://localhost:3001/api/v1/blog", {
blog_title: title,
blog_content: content,
author_id: author,
category_id: category,
})
alert("success add blog")
dispatch({
type: ADD_BLOG,
payload: data,
});
} catch (err) {
dispatch({
payload: err,
});
}
};

I found the solution, so in my action I changed it into:
dispatch({
type: LIST_BLOG,
payload: result.data.data,
});

Related

Build a CRUD App with React Hooks and the Context API

I want to build a CRUD App with React Hooks and the Context API. my problem is in the EditUser component. when I click on edit button to edit user it doesn't show anything on the page and in the console I have this error:
selectedUser is undefined for this part value={selectedUser.name} name="name"
<Input type='text' palaceholder="Enter Name"
value={selectedUser.name} name="name"
onChange={(e) => handleOnChange("name", e.target.value)}
></Input>
thank you for your help!
Here is my components:
GlobalState component:
import React, { createContext, useReducer } from 'react'
import AppReducer from './AppReducer'
//Initial State
const initialState = {
users: [
]
}
//Create Context
export const GlobalContext = createContext(initialState);
//Provider Component
export const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
//Actions
const removeUser = (id) => {
dispatch({
type: 'REMOVE_USER',
payload: id
})
}
const addUser = (user) => {
dispatch({
type: 'ADD_USER',
payload: user
})
}
const editUser = (user) => {
dispatch({
type: 'EDIT_USER',
payload: user
})
}
return (
<GlobalContext.Provider value={{ users: state.users, removeUser, addUser, editUser }}>
{children}
</GlobalContext.Provider>
)
}
AppReducer component:
export default (state, action) => {
switch (action.type) {
case 'REMOVE_USER':
return {
users: state.users.filter(user => {
return user.id !== action.payload
})
}
case 'ADD_USER':
return {
users: [action.payload, ...state.users]
}
case 'EDIT_USER':
const updateUser = action.payload;
const editUsers = state.users.map(user => {
if (user.id === updateUser.id) {
return updateUser
}
return user
});
return {
users: editUsers
}
default:
return state
}
}
and finallyEditUsercomponent:
import { Link, useNavigate, useParams } from 'react-router-dom';
import { GlobalContext } from '../context/GlobalState';
import React,{ useContext, useState, useEffect } from 'react';
import {
Form,
FormGroup,
Label,
Input,
Button
} from 'reactstrap'
const EditUser = () => {
const [selectedUser, setSelectedUser] = useState({
id: "",
name:""
})
const { users, editUser } = useContext(GlobalContext)
const navigate = useNavigate()
const { currentUserId } = useParams();
useEffect(() => {
const userId = currentUserId;
const selectedUser = users.find(user => user.id === parseInt(userId));
setSelectedUser(selectedUser);
}, [currentUserId, users])
const handleSubmit = (e) => {
e.preventDefault();
editUser(selectedUser);
navigate('/');
};
const handleOnChange = (userKey, newValue) => {
setSelectedUser({ ...selectedUser, [userKey]: newValue })
};
return (
<Form onSubmit={handleSubmit}>
<FormGroup>
<Label>Name</Label>
<Input type='text' palaceholder="Enter Name"
value={selectedUser.name} name="name"
onChange={(e) => handleOnChange("name", e.target.value)}
></Input>
</FormGroup>
<Button type='submit' className='bg-success '>Edit Name</Button>
<Link to="/" className="btn btn-danger m-2">Cancel</Link>
</Form>
);
}
export default EditUser;
Issues
Array.prototype.find returns either the first match in the array or undefined if there are no matches. If there is no match found in the array you probably don't want to update the selectedUser state to undefined.
If/when the selectedUser state is undefined attempting to access properties of undefined will throw the error you see. Use a null-check/guard-clause or the Optional Chaining Operator to prevent accidental accesses into potentially null or undefined objects.
Solution
Only enqueue selectedUser updates if there is a matching user to update with.
const { currentUserId } = useParams();
useEffect(() => {
const userId = currentUserId;
const selectedUser = users.find(user => String(user.id) === userId);
if (selectedUser) {
setSelectedUser(selectedUser);
}
}, [currentUserId, users]);
Protect the currentUser nested property accesses, and provide a valid defined fallback value for the input so it doesn't throw errors switching between controlled and uncontrolled. Refactor the handleOnChange to consume the onChange event and destructure the input name and value from it, and use a functional state update to update from the previous state.
const handleOnChange = (e) => {
const { name, value } = e.target;
setSelectedUser(selectedUser => ({
...selectedUser,
[name]: value
}));
};
...
<FormGroup>
<Label>Name</Label>
<Input
type='text'
palaceholder="Enter Name"
value={selectedUser?.name ?? ""}
name="name"
onChange={handleOnChange}
/>
</FormGroup>
Additional Suggestion
This is only a minor point about the reducer function logic. What you have is ok since the only property the userReducer has is a users property, but it's a reducer function convention to also shallow copy the previous state as well.
Example:
export default (state, action) => {
switch (action.type) {
case 'REMOVE_USER':
return {
...state,
users: state.users.filter(user => user.id !== action.payload),
}
case 'ADD_USER':
return {
...state,
users: [action.payload, ...state.users],
}
case 'EDIT_USER':
const updateUser = action.payload;
return {
...state,
users: state.users.map(user => user.id === updateUser.id
? updateUser
: user
),
}
default:
return state;
}
};

update user password using react-router-dom v6

i want to implement update user password form using react-router-dom v6 but this code is not working..
please please.. put your suggestion or explain me about my mistakes on this code.
userReducer.js
import {
UPDATE_PASSWORD_REQUEST,
UPDATE_PASSWORD_SUCCESS,
UPDATE_PASSWORD_RESET,
UPDATE_PASSWORD_FAIL,
CLEAR_ERRORS,
} from "../Constants/userConstant";
export const profileReducer = (state = {}, action) => {
switch (action.type) {
case UPDATE_PASSWORD_REQUEST:
return {
...state,
loading: true,
};
case UPDATE_PASSWORD_SUCCESS:
return {
...state,
loading: false,
isUpdated: action.payload,
};
case UPDATE_PASSWORD_FAIL:
return {
...state,
loading: false,
error: action.payload,
};
case UPDATE_PASSWORD_RESET:
return {
...state,
isUpdated: false,
};
case CLEAR_ERRORS:
return {
...state,
error: null,
};
default:
return state;
}
};
userAction.js
import {
UPDATE_PASSWORD_REQUEST,
UPDATE_PASSWORD_SUCCESS,
UPDATE_PASSWORD_FAIL,
CLEAR_ERRORS,
} from "../Constants/userConstant";
export const updatePassword = (passwords) => async (dispatch) => {
try {
dispatch({ type: UPDATE_PASSWORD_REQUEST });
const config = { headers: { "Content-Type": "application/json" } };
const { data } = await axios.put(
`/api/v1/password/update`,
passwords,
config
);
dispatch({ type: UPDATE_PASSWORD_SUCCESS, payload: data.success });
} catch (error) {
dispatch({
type: UPDATE_PASSWORD_FAIL,
payload: error.response.data.message,
});
}
};
export const clearErrors = () => async (dispatch) => {
dispatch({ type: CLEAR_ERRORS });
};
store.js
import {createStore,combineReducers,applyMiddleware} from 'redux';
import thunk from "redux-thunk";
import {composeWithDevTools} from "redux-devtools-extension";
import { profileReducer } from './Reducers/userReducer';
const reducer = combineReducers({
profile:profileReducer,
})
let initialState = {};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
UpdatePassword.js
import React, { Fragment, useState, useEffect } from "react";
import "./UpdatePassword.css";
import Loader from "../Loader/Loader";
import { useDispatch, useSelector } from "react-redux";
import { clearErrors, updatePassword } from "../../Actions/userAction";
import { UPDATE_PASSWORD_RESET } from "../../Constants/userConstant";
import {useNavigate} from 'react-router-dom'
const UpdatePassword = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { error, isUpdated, loading } = useSelector((state) => state.profile);
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const updatePasswordSubmit = (e) => {
e.preventDefault();
const myForm = new FormData();
myForm.set("oldPassword", oldPassword);
myForm.set("newPassword", newPassword);
myForm.set("confirmPassword", confirmPassword);
dispatch(updatePassword(myForm));
};
useEffect(() => {
if (error) {
alert(error);
dispatch(clearErrors());
}
if (isUpdated) {
alert("Profile Updated Successfully");
navigate("/account");
dispatch({
type: UPDATE_PASSWORD_RESET,
});
}
}, [dispatch, error, isUpdated]);
return (
<Fragment>
{loading ? (
<Loader />
) : (
<Fragment>
{/* <MetaData title="Change Password" /> */}
<div className="updatePasswordContainer">
<div className="updatePasswordBox">
<h2 className="updatePasswordHeading">Update Profile</h2>
<form
className="updatePasswordForm"
onSubmit={updatePasswordSubmit}
>
<div className="loginPassword">
<input
type="password"
placeholder="Old Password"
required
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
/>
</div>
<div className="loginPassword">
<input
type="password"
placeholder="New Password"
required
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</div>
<div className="loginPassword">
<input
type="password"
placeholder="Confirm Password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<input
type="submit"
value="Change"
className="updatePasswordBtn"
/>
</form>
</div>
</div>
</Fragment>
)}
</Fragment>
);
};
export default UpdatePassword;
i want to make a form where user update user password.but due to any mistake this form is not working...

Context & Reducer not returning State

Could someone please let me know why the state isn't being updated from the reducer? The useEffect(()=>{}) isn't being triggered when the state is being returned from the reducer. I have validated the correct information is being passed to the return, but nothing can be seen from the LoginScreen.
Context Script
import React, { createContext, useReducer } from "react";
import userReducer from "./UserReducer";
export const UserContext = createContext();
const initialState = {
userData: [],
isLoggedIn: false,
isAdmin: false,
isEmployee: false,
errorMessage: [{ success: false, statusCode: 0, error: null }],
};
const UserContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, initialState);
const registerUser = (user) =>
dispatch({ type: "REGISTER_USER", payload: user });
const loginUser = (user) => dispatch({ type: "LOGIN_USER", payload: user });
const deleteUser = (user) => dispatch({ type: "DELETE_USER", payload: user });
const updateUser = (user) => dispatch({ type: "UPDATE_USER", payload: user });
const contextValues = {
...state,
registerUser,
loginUser,
deleteUser,
updateUser,
};
return (
<UserContext.Provider value={contextValues}>
{children}
</UserContext.Provider>
);
};
export default UserContextProvider;
Reducer Script
import axios from "axios";
axios.defaults.withCredentials = true;
const userReducer = (state = {}, action) => {
let config = {
header: {
"Content-Type": "application/json",
},
};
switch (action.type) {
case "REGISTER_USER":
break;
case "LOGIN_USER":
console.log(state);
const email = action.payload.email;
const password = action.payload.password;
axios
.post("/api/user/login", { email, password }, config)
.then((response) => {
if (response.data.success) {
// localStorage.setItem("authToken", response.data.authToken);
state.userData = response.data.user;
state.isLoggedIn = true;
if (response.data.user.role === 9) {
state.isAdmin = true;
state.isEmployee = true;
} else {
state.isAdmin = false;
state.isEmployee = false;
}
}
})
.catch((error) => {
state.errorMessage = {
success: error.response.data.success,
statusCode: error.response.status,
message: error.response.data.error,
};
});
return {
...state,
userData: [state.userData],
isLoggedIn: state.isLoggedIn,
isAdmin: state.isAdmin,
isEmployee: state.isEmployee,
errorMessage: [state.errorMessage],
};
default:
return state;
}
};
export default userReducer;
Login Form
import { useState, useEffect, useContext } from "react";
import { Link } from "react-router-dom";
import {
Button,
Form,
Grid,
Message,
Segment,
Image,
Container,
} from "semantic-ui-react";
//Custom Imports
import "./LoginScreen.css";
import Logo from "../../../img/logo.png";
//Context
import { UserContext } from "../../context/UserContext";
const LoginScreen = ({ history }) => {
const { userData, loginUser, isLoggedIn, errorMessage, clearErrorMessage } =
useContext(UserContext);
const [user, setUser] = useState({ email: "", password: "" });
const [error, setError] = useState("");
useEffect(() => {
console.log(errorMessage);
if (localStorage.getItem("authToken")) {
history.push("/dashboard");
}
}, [history]);
useEffect(() => {
if (isLoggedIn) {
console.log(userData);
console.log("User is Logged in");
// history.push("/");
}
if (!errorMessage.success && errorMessage.error != null) {
console.log(errorMessage);
setError(errorMessage.message);
setTimeout(() => {
setError("");
}, 5000);
}
}, [userData, errorMessage, isLoggedIn]);
return (
<Container className="login-container">
<Grid
textAlign="center"
style={{ height: "100vh" }}
verticalAlign="middle"
>
<Grid.Column style={{ maxWidth: 450 }}>
<Image src={Logo} className="login-logo" />
<Form size="large" onSubmit={() => loginUser(user)}>
<Segment stacked>
<Form.Input
fluid
icon="user"
iconPosition="left"
placeholder="Email Address"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
/>
<Form.Input
fluid
icon="lock"
iconPosition="left"
placeholder="Password"
value={user.password}
type="password"
onChange={(e) => setUser({ ...user, password: e.target.value })}
/>
{error && <span>{error}</span>}
<Button color="blue" fluid size="large" type="submit">
Login
</Button>
</Segment>
</Form>
<Message>
Don't have an account? <Link to="/register">Sign Up</Link>
</Message>
</Grid.Column>
</Grid>
</Container>
);
};
export default LoginScreen;
Refactor your login function like this
const loginUser({ email, password }) => {
let config = {
header: {
"Content-Type": "application/json",
},
};
axios
.post("/api/user/login", { email, password }, config)
.then((response) => {
if (response.data.success) {
dispatch({ type: 'LOGIN_SUCCESS', payload: response.data });
}
})
.catch((error) => {
dispatch({ type: 'LOGIN_FAILED', payload: error });
});
}
and then your reducer
...
switch(action.type) {
...
case 'LOGIN_SUCCESS':
// return here a new object
// do not mutate the state (state.something = something) is not allowed
...
case 'LOGIN_FAILED':
// handle error
}
Prerequisite Reducer Concepts
Redux and useReducer use reducer like (previousState, action) => newState.
The reducer should be a 'pure' function as in this document. The promises, api calls should not be use inside reducers.
The problem:
Because you call api/promise inside the reducer. The reducer function returns the value before the promise finish. So when the promise finishes, nothing happen.
// A will be return before B, C are going to call
case "LOGIN_USER":
promiseFn()
.then(/* B */ ...)
.catch(/* C */ ...)
// A
return {
...
}
Solution:
Separate the non-pure calls from the reducer. And put them in the other code blocks (like inside hooks, event handlers...).

POST request using React with Hooks and Redux

I am making a React app where I need to add Redux using Hooks. Currently, I am stuck in making a POST request and can't figure out after going through the internet, how to make it work. I am on my way to understand how the Redux works and I will be happy for any help on this to make it work, so I can understand what is missing and how to send the data. My components:
App.js:
import { useState } from "react";
import { connect } from 'react-redux';
import "./App.css";
import Posts from "./components/posts";
import { addPost } from "./store/actions/postAction";
function App() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
const post = {
title: title,
body: body,
}
addPost(post);
setTitle('');
setBody('');
alert("Post added!");
};
return (
<div className="App">
<Posts />
<form onSubmit={handleSubmit}>
<label>
Mew post:
<input
type="text"
name="title"
placeholder="Add title"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<input
type="text"
name="body"
placeholder="Add body"
value={body}
onChange={e => setBody(e.target.value)}
/>
</label>
<button type="submit">Add</button>
</form>
</div>
);
}
export default connect()(App);
postAction.js
import axios from "axios";
import { GET_POSTS, ADD_POST, POSTS_ERROR } from "../types";
const url = "http://localhost:8002/";
export const getPosts = () => async (dispatch) => {
try {
const response = await axios.get(`${url}posts`);
dispatch({
type: GET_POSTS,
payload: response.data,
});
} catch (error) {
dispatch({
type: POSTS_ERROR,
payload: error,
});
}
};
export const addPost = (post) => (dispatch) => {
try {
const response = axios.post(`${url}`, {post});
dispatch({
type: ADD_POST,
payload: response.data,
});
} catch (error) {
dispatch({
type: POSTS_ERROR,
payload: error,
});
}
};
postReducer.js
import { ADD_POST, GET_POSTS, POSTS_ERROR } from "../types";
const initialState = {
posts: []
};
const postReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POSTS:
return {
...state,
posts: action.payload
};
case ADD_POST:
return {
...state,
posts: action.payload
};
case POSTS_ERROR:
return {
error: action.payload
};
default:
return state;
}
};
export default postReducer;
posts.js
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getPosts } from "../store/actions/postAction";
const Posts = () => {
const dispatch = useDispatch();
const postsList = useSelector((state) => state.postsList);
const { loading, error, posts } = postsList;
useEffect(() => {
dispatch(getPosts());
}, [dispatch]);
return (
<>
{loading
? "Loading..."
: error
? error.message
: posts.map((post) => (
<div className="post" key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
))}
</>
);
};
export default Posts;
App.js -> change to export default connect(null, {addPost})(App);

Writing tests with react and context api

I'm trying to write a test and I'm not getting the result thats its intended, can someone see what I'm doing wrong here please? I'm trying to call my addTodo but all I get is the error message below, quite new to testing so I'm not sure what it means.
my error message is this:
● <TodoForm /> #addTodo
expect(jest.fn()).toBeCalledWith(...expected)
Expected: {"payload": "a new todo", "type": "addTodo"}
Number of calls: 0
24 | form.find("button").simulate("click");
25 |
> 26 | expect(dispatch).toBeCalledWith({ type: "addTodo", payload: "a new todo" });
| ^
27 | });
28 |
at Object.<anonymous> (src/tests/TodoForm.test.js:26:20)
Here the relevant files:
the test: TodoForm.test.js
import React from "react";
import Enzyme, { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { Provider } from "../context/TodoContext";
import TodoForm from "../components/TodoForm";
Enzyme.configure({ adapter: new Adapter() });
test("<TodoForm /> #addTodo", async () => {
const dispatch = jest.fn();
const form = mount(
<Provider value={{ dispatch }}>
<TodoForm />
</Provider>
);
form.find("input").simulate("change", { target: { value: "a new todo" } });
form.find("button").simulate("click");
expect(dispatch).toBeCalledWith({ type: "addTodo", payload: "a new todo" });
});
the file I'm trying to write the test for:
import React, { useContext, useState } from "react";
import { Context } from "../context/TodoContext";
import "../css/TodoForm.css";
const TodoForm = ({ initialValues }) => {
const { addTodo } = useContext(Context);
const [title, setTitle] = useState("");
const [error, setError] = useState("");
function handleTodoAdd() {
if (title === "") {
setError(true);
} else {
setError(false);
}
setTitle("");
}
const onChange = e => {
setTitle(e.target.value);
};
/*
function handleSubmitForm(event) {
if (event.keyCode === 13) handleTodoAdd();
}
*/
return (
<div className="container">
<div className="inputContainer">
<div className="input-group">
<input
autoFocus={true}
aria-label="Enter the title of your todo"
placeholder="Enter new todo"
value={title}
onChange={onChange}
/>
<div className="errorContainer">
<span className="error">
{error ? "Please enter a value" : null}
</span>
</div>
<div className="input-group-append">
<button
aria-label="Add todo to your list"
className="addButton"
onClick={() => addTodo(title)}
>
Add
</button>
</div>
</div>
</div>
</div>
);
};
TodoForm.defaultProps = {
initialValues: {
title: "adsda"
}
};
export default TodoForm;
TodoContext.js I'm using this file to pull my provider
import React from "react";
import createDataContext from "./createDataContext";
export const TodoContext = React.createContext();
export default function todoReducer(state, action) {
switch (action.type) {
case "addTodo":
return [
...state,
{ id: Math.floor(Math.random() * 999), title: action.payload }
];
case "deleteTodo":
return state.filter(todo => todo.id !== action.payload);
case "editTodo":
return state.map(todo => {
return todo.id === action.payload.id ? action.payload : todo;
});
default:
return state;
}
}
const addTodo = dispatch => {
return title => {
dispatch({ type: "addTodo", payload: title });
};
};
const deleteTodo = dispatch => {
return id => {
dispatch({ type: "deleteTodo", payload: id });
};
};
const editTodo = dispatch => {
return (id, title) => {
dispatch({ type: "editTodo", payload: { id, title } });
};
};
export const { Context, Provider } = createDataContext(
todoReducer,
{ addTodo, deleteTodo, editTodo },
[]
);
auto creates my context data
import React, { useReducer } from "react";
export default (reducer, actions, initialState) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};

Resources