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;
}
};
Related
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,
});
I have a form that adds new articles. I need to create another form that triggers when I click on a created article and add a property "keyword" to the article state and display it. I tried to do something but I am kinda stuck.
Form.jsx component that adds the article/s:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { v1 as uuidv1 } from 'uuid';
import { ADD_ARTICLE } from '../constants/action-types';
const Form = () => {
const [title, setTitle] = useState('');
const dispatch = useDispatch();
const handleChange = (e) => {
const { value } = e.target
setTitle(value);
}
const handleSubmit = (e) => {
e.preventDefault();
const id = uuidv1();
dispatch({ type: ADD_ARTICLE, payload: { id, title } });
setTitle('');
}
return (
<form onSubmit={handleSubmit}>
<div className='form-group'>
<label htmlFor='title'>Title</label>
<input
type='text'
className='form-control'
id='title'
value={title}
onChange={handleChange}
/>
</div>
<input className='btn btn-success btn-lg' type='submit' value='SAVE' />
</form>
);
}
export default Form;
List.jsx component where the articles are displayed:
import React, { useState,useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import KeywordForm from './KeywordForm.jsx';
import { fetchArticles } from '../thunk';
const List = () => {
const [showForm,setShowForm]=useState(false);
const articles = useSelector(state => state.articles);
const dispatch = useDispatch();
const displayForm=()=>{
setShowForm(!showForm)
}
useEffect(() => {
dispatch(fetchArticles);
}, []);
return (
<>
<ul className='list-group list-group-flush'>
{articles.map(article => (
<li className='list-group-item' key={article.id} onClick={displayForm}>
{article.title}
</li>
))}
</ul>
<div>
{showForm && (
<KeywordForm />
)}
</div>
</>
);
}
export default List;
Here i added a state that displays the KeywordForm component when I click an article.
KeywordForm.jsx component,this is the one that I created to add the keyword:
import React, { useState } from 'react';
import { useDispatch ,useSelector} from 'react-redux';
import { ADD_KEYWORD } from '../constants/action-types';
const KeywordForm = ({id,title}) => {
const [keyword,setKeyword]=useState('');
const articles = useSelector(state => state.articles);
const dispatch=useDispatch();
console.log(articles)
const handleChange = (e) => {
const { value } = e.target
setKeyword(value);
}
const handleSubmit = (e) => {
e.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<div className='form-group'>
<label htmlFor='keyword'>Keyword</label>
<input
type='text'
className='form-control'
id='keyword'
value={keyword}
onChange={handleChange}
/>
</div>
<input className='btn btn-success btn-lg' type='submit' value='SAVE' />
</form>
);
}
export default KeywordForm;
reducers.js
const initialState = {
articles: []
};
const rootReducer = (state = initialState, action) => {
const { type, payload } = action;
switch(type) {
case ADD_ARTICLE: {
return {...state,
articles: [...state.articles,payload]
};
}
case ADD_KEYWORD: {
return Object.assign({}, state, {
articles: state.articles.concat(payload)
});
}
case ARTICLES_RETRIEVED: {
return Object.assign({}, state, {
articles: state.articles.concat(payload)
});
}
default:
return state;
}
}
export default rootReducer;
actions.js
import { ADD_ARTICLE, ARTICLES_RETRIEVED,ADD_KEYWORD } from '../constants/action-types';
const addArticle = (payload) => {
return { type: ADD_ARTICLE, payload };
}
const addKeyword = (payload) => {
return { type: ADD_KEYWORD, payload };
}
const articlesRetrieved = (payload) => {
return { type: ARTICLES_RETRIEVED, payload };
}
export { addArticle, articlesRetrieved,addKeyword };
What should i add to my reducers/actions to make this work? My idea is that i have to somehow pass the id of the article clicked and then in the reducer find it's index or something and check it with the payload.id .
You want to modify an existing article in the state and a keyword to it (can there be an array of keywords, or just one?). In order to do that, your action payload will need to contain both the keyword and the id of the article that it belongs to.
Your reducer will find the article that matches the id and replace it with a copied version that has the keyword added to it.
case ADD_KEYWORD: {
return {
...state,
articles: state.articles.map(article =>
// find the article to update
article.id === payload.id ?
// update it
{ ...article, keyword: payload.keyword } :
// otherwise return the original
article
}
}
This is easier to do with the official Redux Toolkit because you can modify the draft state directly and you don't need to worry about mutations.
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);
I have a functional login page connected with redux, I'm firing an async event onSubmit that will trigger the emailLogin action, I am using useEffect to detect the change of the isLoading prop to see whether login finished or not. If login success, the redux store should have the user object, if failed, the user should remain null.
The question is, I know that the login is success, which should triggered the change of isLoading, the parameter that decide whether the useEffect, however, the useEffect is not fired. Also, the console.log('done'); after the line await emailLogin(authData); is never fired. Ssomething is wrong.
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { Link, useHistory } from 'react-router-dom';
import { emailLogin } from '../actions/index';
function Login({ user, isLoading, emailLogin }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const history = useHistory();
useEffect(() => {
console.log('useEffect fired', user, isLoading); //<-----This does not fire after login success
if (user) {
history.push('/protected_home');
}
}, [isLoading]);
const submitEmailLoginForm = async (e) => {
e.preventDefault();
const authData = { email, password };
await emailLogin(authData);
console.log('done'); // <------- This is never fired
};
return (
<div>
<h2>Login</h2>
<Link to="/">back</Link>
<form onSubmit={submitEmailLoginForm}>
<label>
email:
<input
type="text"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
<label>
password:
<input
type="text"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<input type="submit" value="Submit" />
</form>
</div>
);
}
const mapStateToProps = (state) => ({
user: state.user,
isLoading: state.isLoading
});
const mapDispatch = {
emailLogin: emailLogin
};
export default connect(mapStateToProps, mapDispatch)(Login);
My action file:
import axios from 'axios';
export const authActions = {
EMAIL_LOGIN_START: '##EMAIL_LOGIN_START',
EMAIL_LOGIN_SUCCESS: '##EMAIL_LOGIN_SUCCESS'
};
export const emailLogin = ({ email, password }) => async (dispatch) => {
dispatch({ type: authActions.EMAIL_LOGIN_START });
try {
const response = await axios.post('http://localhost:5001/api/auth', {
email: email,
password: password
});
dispatch({
type: authActions.EMAIL_LOGIN_SUCCESS,
payload: {
user: { ...response.data }
}
});
} catch (error) {
console.log('Should dispatch api error', error.response);
}
};
My Reducer:
import { authActions } from '../actions/index';
const initialState = {
user: null,
isLoading: false
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case authActions.EMAIL_LOGIN_START:
return { ...state, isLoading: true };
case authActions.EMAIL_LOGIN_SUCCESS:
console.log('Reducer check => Login is success'); //<-----this line is printed
return { ...state, user: action.payload.user, isLoading: false };
default:
return state;
}
};
export default userReducer;
In the reducer, I see that the success action is actually triggered by checking the console.log(). Also in the redux dev tool, I can actually see that the login is success and the isLoading prop has changed :
This solve my problem
const mapStateToProps = (state) => ({
user: state.userReducer.user,
isLoading: state.userReducer.isLoading
});
I have a file called Login.js in the file I call this.props.onUserLogin and pass the users auth token. I can verify that I am getting the token from the api call via the console log. When I get to the reducer I loose the value from the state somehow. I can verify that with the console log as well. I am trying to figure out why my state.loginToken is empty in the reducer.
Login.js
import React, {Component} from 'react';
import * as actionTypes from '../Store/actions';
import UserInfoButton from '../UserInfoButton/UserInfoButton';
import { connect } from 'react-redux';
const axios = require('axios');
class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
};
}
handleChange = (event) => {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value
})
}
handleLogin = (event) => {
axios.post('http://127.0.0.1:8000/api/user/token/', {
email: this.state.email,
password: this.state.password,
}).then( (response) => {
console.log('response: ' + response.data.token);
this.props.onUserLogin(response.data.token);
})
.catch( (error) => {
console.log(error)
})
event.preventDefault();
}
render() {
const isLoggedIn = this.props.loginToken;
let userButton;
if(isLoggedIn) {
userButton = <UserInfoButton/>
} else {
userButton = "";
}
return (
<div>
<form onSubmit={this.handleLogin}>
Email:<br/>
<input type="text" value={this.state.email} onChange={this.handleChange} name="email"/><br/>
Password:<br/>
<input type="password" value={this.state.password} onChange={this.handleChange} name="password"/><br/>
<br/>
<input type="submit" value="Submit"></input>
</form>
<div>{this.props.loginToken}</div>
<br/>
<div>{userButton}</div>
</div>
)
}
}
const mapStateToProps = state => {
return {
loginToken: state.loginToken
};
}
const mapDispatchToProps = dispatch => {
return {
onUserLogin: (userToken) => dispatch( { type: actionTypes.USER_LOGIN_ACTION, loginToken: userToken } ),
};
}
export default connect(mapStateToProps,mapDispatchToProps)(Login);
reducer.js
import * as actionTypes from './actions';
const initialState = {
loginToken: '',
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case actionTypes.USER_LOGIN_ACTION:
console.log('action: ' + state.loginToken);
return {
...state,
loginToken: state.loginToken,
}
default:
return state;
}
};
export default reducer;
Basically a typo inside of your reducer - You're looking for the loginToken inside of state instead of the action.
case actionTypes.USER_LOGIN_ACTION:
console.log('action: ' + action.loginToken); // not state.loginToken
return {
...state,
loginToken: action.loginToken, // not state.loginToken
}