I am learning Redux so this may be a basic question and answer but here we go.
I am trying to fetch a list of items from my backend api (/api/items) and pass them into my ShoppingList.js component so that when the component loads the list of items will be displayed. I think that I have to fetch the data in the action, and then call that action in the useEffect hook in the react component, but I am not too sure as I can't seem to get it to work.
Can anyone help me figure this out?
Redux.js
import { createStore } from 'redux';
import axios from 'axios';
const initialState = {
items: [],
loading: false,
};
export const store = createStore(
reducer,
initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
function reducer(state, action) {
switch (action.type) {
case 'GET_ITEMS':
return {
...state,
items: action.payload,
loading: false,
};
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
};
case 'DELETE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
};
case 'ITEMS_LOADING':
return {
...this.state,
loading: true,
};
default:
return state;
}
}
export const getItemsAction = () => ({
return(dispatch) {
console.log('here');
axios.get('api/items').then(response => {
console.log(response);
dispatch({ type: 'GET_ITEMS', payload: response.data });
});
},
});
export const addItemAction = item => ({
type: 'ADD_ITEM',
payload: item,
});
export const deleteItemAction = item => ({
type: 'DELETE_ITEM',
payload: item,
});
export const setItemsLoading = () => ({
type: 'ITEMS_LOADING',
});
ShoppingList.js
export default function ShoppingList() {
const items = useSelector(state => state.items);
const dispatch = useDispatch();
const addItem = name => dispatch(addItemAction(name));
const deleteItem = id => dispatch(deleteItemAction(id));
useEffect(() => {
//call get items dispatch?
getItemsAction();
});
return (
<div className="container mx-auto">
<button
onClick={() => {
const name = prompt('Enter Item');
if (name) {
// setItems([...items, { id: uuid(), name: name }]);
addItem({
id: uuid(),
name: name,
});
}
}}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-4"
>
Add Item
</button>
<ul className="mt-4">
<TransitionGroup className="shopping-list">
{items.map(({ id, name }) => (
<CSSTransition
key={id}
timeout={500}
classNames="fade"
style={{ marginBottom: '0.5rem' }}
>
<li>
{' '}
<button
className="bg-red-500 rounded px-2 mr-2 text-white"
onClick={deleteItem.bind(this, id)}
>
×
</button>
{name}
</li>
</CSSTransition>
))}
</TransitionGroup>
</ul>
</div>
);
}
You are close to where you need to be, the missing piece is that redux is synchronous, so you need to use something like redux-thunk or redux-saga to handle async actions such as network requests.
Once you have setup whatever library you want, you would call it similarly to calling a redux action, from a useEffect like you suggest.
So for example, if you use a thunk:
export const getItemsAction = () => ({
return (dispatch) => {
axios.get('api/items').then(response => {
console.log(response)
dispatch({ type: 'GET_ITEMS_SUCCESS', payload: response.data })
}).catch(err => dispatch({ type: 'GET_ITEMS_ERROR', error: err })
},
})
Then you could call it from your effect:
useEffect(() => {
dispatch(getItemsAction())
}, []);
Make sure you add an empty dependency array if you only want to call it once, otherwise it will get called every time your component refreshes.
Note that the thunk is able to dispatch other redux actions, and I changed a few of your actions types to make it more clear what is going on;
GET_ITEMS_SUCCESS is setting your successful response & GET_ITEMS_ERROR sets any error if there is one.
Related
I'm using react with Context Api and react hook form to add a new post. When I submit a post, the new post is in the last position but when I refresh the page, the post goes to the top position. I want the new post to be in the top position immediately. Do you know what I did wrong, please?
PostsContext.tsx
export interface Post {
id: number
content: string
thumbnail: {
url: string
}
created_at: string
updated_at: string
userId: number
}
export interface PostsState {
posts: Post[]
}
export type PostsAction =
| { type: 'SET_POSTS'; payload: Post[] }
| { type: 'ADD_POST'; payload: Post }
const initialState: PostsState = {
posts: [],
}
const reducer = (state: PostsState, action: PostsAction) => {
switch (action.type) {
case 'SET_POSTS':
return { posts: action.payload }
case 'ADD_POST':
return { posts: [...state.posts, action.payload] }
default:
return state
}
}
export const PostsContext = createContext<{
state: PostsState
dispatch: React.Dispatch<PostsAction>
}>({
state: initialState,
dispatch: () => null,
})
export const PostsProvider = ({ children }: PropsWithChildren<{}>) => {
const [state, dispatch] = useReducer(reducer, initialState)
return <PostsContext.Provider value={{ state, dispatch }}>{children}</PostsContext.Provider>
}
Feed.tsx
const Feed = () => {
const { state, dispatch } = usePostsContext()
useEffect(() => {
const fetchPosts = async () => {
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/posts`, {
credentials: 'include',
})
const data = await res.json()
if (res.ok) {
dispatch({ type: 'SET_POSTS', payload: data })
}
}
fetchPosts()
}, [])
return (
<div className="container mx-auto">
<PostForm />
{state.posts.length < 1 ? (
<div className="mt-4 p-8 text-center border border-gray-200 rounded-lg">
<h2 className="text-2xl font-medium">There's nothing here...</h2>
<p className="mt-4 text-sm text-gray-500">
Created posts will appear here, try creating one!
</p>
</div>
) : (
state.posts.map((post) => <Posts key={post.id} post={post} />)
)}
</div>
)
}
export default Feed
PostForm.tsx
const PostForm = () => {
const { dispatch } = usePostsContext()
const {
register,
handleSubmit,
control,
watch,
reset,
formState: { isSubmitSuccessful, errors },
} = useForm<FormInput>({
defaultValues: {
content: '',
},
resolver: yupResolver(postSchema),
})
const selectedFile = watch('thumbnailFile')
const onSubmit: SubmitHandler<FormInput> = async (data) => {
const formData = new FormData()
formData.append('content', data.content)
formData.append('thumbnailFile', data.thumbnailFile[0])
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/posts`, {
method: 'POST',
credentials: 'include',
body: formData,
})
const post = await response.json()
if (response.ok) {
console.log('post created', post)
dispatch({ type: 'ADD_POST', payload: post })
reset()
}
}
return (
<MyForm />
)
}
export default PostForm
Looks like your ADD_POST reducer is adding a new post to the end of the list since you are placing the action payload after the destructing of your old posts.
To place a post at the beginning of the list you need to place the action payload before destructing the old list of posts.
i.e.,
case 'ADD_POST':
return { posts: [action.payload, ...state.posts] }
I am in the process of finishing my todo application, since redux is a bit new to me of course I encounter problems. My problem is when I click the delete button (recycle icon) I want a confirmation box to pop up with basic Yes and No buttons, I have built that, but... when I click one recycle button all of the other confirmation boxes get set to True and they pop up in sync. I need only one box to pop up for the right todo.id.
Note: I have built this before without redux, but I am still wrapping my head around redux.
Here is my code:
JS:
import React, {useState, Fragment} from 'react'
import { useDispatch, useSelector } from 'react-redux';
import "./todo.css"
const Todos = () => {
const dispatch = useDispatch();
const todos = useSelector(state => state.todos);
const confirmationSt = useSelector(state => state.confirmation)
const handleConfirm = id => {
dispatch({
type: "CONFIRM",
})
}
const handleContinue = () => {
dispatch({
type: "CONTINUE",
})
}
const handleClick = id => dispatch({
type: "DELETE_TODO",
payload: id,
})
if (!todos || !todos.length) {
return <p>Empty</p>
}
return (
<ul className='unlist'>{todos.map(todo =><Fragment key={todo.id}> <div className='todoContent'><li >{todo.label}</li>
<div><button className='delete' onClick={handleConfirm}><i className="fas fa-recycle"></i></button>
<button className='delete' onClick={handleConfirm}><i className="fas fa-wrench"></i></button>
</div>
</div>
{confirmationSt === true ? <div className='confirmation-box'>
Are you sure?
<button onClick={() => handleClick(todo.id)}>Yes</button>
<button onClick={handleContinue}>No</button>
</div> : null}
</Fragment>
)}
</ul>
)
}
Reducer:
const initalState = {
todos: [],
confirmation: false,
}
const reducer = (state = initalState, action) => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [...state.todos, action.payload],
}
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
}
case "CONFIRM":
return {
...state,
confirmation: !state.confirmation,
}
case "CONTINUE":
return {
...state,
confirmation: false
}
default: return state;
}
}
export default reducer
I have changed confirmation from boolean to id of the task
import React, { useState, Fragment } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import "./todo.css"
const Todos = () => {
const dispatch = useDispatch();
const todos = useSelector(state => state.todos);
const confirmationSt = useSelector(state => state.confirmation)
const handleConfirm = id => {
dispatch({
type: "CONFIRM",
payload: id
})
}
const handleContinue = () => {
dispatch({
type: "CONTINUE",
})
}
const handleClick = id => dispatch({
type: "DELETE_TODO",
payload: id,
})
if (!todos || !todos.length) {
return <p>Empty</p>
}
return (
<ul className='unlist'>
{ todos.map(todo =>
<Fragment key={todo.id}>
<div className='todoContent'>
<li >{todo.label}</li>
<div>
<button className='delete' onClick={()=>handleConfirm(todo.id)}>
<i className="fas fa-recycle"></i>
</button>
<button className='delete' onClick={()=>handleConfirm(todo.id)}>
<i className="fas fa-wrench"></i>
</button>
</div>
</div>
{
confirmationSt === todo.id ?
<div className='confirmation-box'>
Are you sure?
<button onClick={() => handleClick(todo.id)}>Yes</button>
<button onClick={handleContinue}>No</button>
</div>
: null
}
</Fragment>
)}
</ul>
)
}
const initalState = {
todos: [],
confirmation: -1,
}
const reducer = (state = initalState, action) => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [...state.todos, action.payload],
}
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
}
case "CONFIRM":
return {
...state,
confirmation: action.payload,
}
case "CONTINUE":
return {
...state,
confirmation: false
}
default: return state;
}
}
export default reducer
if you know react (using context api) ,you can look into my issue
this is my github repo : https://github.com/nareshkumar1201/todo-react
i am facing issue , where i wanted to update/toggle the completed property in todo list ---
present state of the app is that -one can add todo ,delete todo , and now i am working on the checkbox -on click of checkbox it should update the completed property in todo list , issue is that when i click on checkbox for the first time it is working fine but if is select/uncheck the todo for the 2nd time its not working ... i don,t know where i am going wrong ??
it will a great help if some one look into issue
import React, { Fragment, useContext } from "react";
import PropTypes from "prop-types";
import TodoContext from "../context/TodoContext";
const TodoItem = ({ todo }) => {
// console.log(todo);
const todoContext = useContext(TodoContext);
const { deleteTodo, updateTodo } = todoContext;
// const { id, todo, completed } = todo;
**const onChange = (e) => {
console.log("todoItem: update req", todo.id);
updateTodo(todo.id);
};**
const onClick = () => {
console.log("todoItem: delete req", todo.id);
deleteTodo(todo.id);
};
return (
<Fragment>
<div className="container col-12 todo-item-container">
<ul className="list-group w-100">
<li className="list-group-item">
**<input
type="checkbox"
name="todo"
// value={todo.todo}
onChange={onChange}
/>**
<span className="text-info"> {todo.todo}</span>
<button className="btn float-right ">
<i
className="fa fa-pencil-square-o text-info "
aria-hidden="true"
></i>
</button>
<button className="btn float-right" onClick={onClick}>
{" "}
<i className="fa fa-trash text-info" aria-hidden="true"></i>
</button>
</li>
</ul>
</div>
</Fragment>
);
};
TodoItem.propTypes = {
todo: PropTypes.object.isRequired,
};
export default TodoItem;
This is state:
import React, { useReducer } from "react";
import TodoContext from "./TodoContext";
import TodoReducer from "./TodoReducer";
import { v1 as uuid } from "uuid";
import {
ADD_TODO,
EDIT_TODO,
DELETE_TODO,
DISPLAY_TODOS,
UPDATE_TODO,
} from "./types";
const TodoState = (props) => {
const initialState = {
todos: [
{
id: 1,
todo: "Do cook food",
completed: false,
},
{
id: 2,
todo: "Clean Room",
completed: false,
},
{
id: 3,
todo: "Wash Car",
completed: false,
},
],
};
const [state, dispatch] = useReducer(TodoReducer, initialState);
//add todo
const addTodo = (todo) => {
todo.id = uuid();
console.log(todo);
// todo.completed = false;
dispatch({ type: ADD_TODO, payload: todo });
};
//display todo
const displayTodos = () => {
dispatch({ type: DISPLAY_TODOS, payload: state.todos });
};
//edit todo
//delete todo
const deleteTodo = (id) => {
console.log("payload id", id);
dispatch({ type: DELETE_TODO, payload: id });
};
//update todo
**const updateTodo = (id) => {
console.log("in todoState", id);
dispatch({ type: UPDATE_TODO, payload: id });
};**
return (
<TodoContext.Provider
value={{
todos: state.todos,
addTodo,
displayTodos,
deleteTodo,
updateTodo,
}}
>
{props.children}
</TodoContext.Provider>
);
};
export default TodoState;
This is reducer:
import {
ADD_TODO,
EDIT_TODO,
DELETE_TODO,
DISPLAY_TODOS,
UPDATE_TODO,
} from "./types";
// import todoContext from "./TodoContext";
const TodoReducer = (state, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload],
};
case DISPLAY_TODOS:
return {
...state,
todos: action.payload,
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter((todoObj) => todoObj.id !== action.payload),
};
**case UPDATE_TODO:
console.log("in reducer :", action.payload);
return {
...state,
todos: state.todos.map((todoObj) => {
if (todoObj.id === action.payload) {
todoObj.completed = !todoObj.completed;
}
return todoObj;
}),
};**
default:
return state;
}
};
export default TodoReducer;
Screen images:on click on checkbox for the first time -working fine
if click on 2nd todo checkbox its not working
There was issue with your return in UPDATE_TODO.
I have fixed same for you, Do test and let me know.
While making changes to the object in todos array you need to copy it using the spread operator and then make changes and then return the object.
case UPDATE_TODO:
console.log("in reducer :", action.payload);
return {
...state,
todos: state.todos.map((todoObj) => {
if (todoObj.id === action.payload) {
return { ...todoObj, completed: !todoObj.completed };
}
return todoObj;
}),
};
I've created three useSelector hooks and for the first one I change the value by dispatching an action with an OnClick function. When I do this my other useSelects get re-rendered even though the reference hasn't changed. Does anyone know why this is happening? I believe this is happening because I put console.logs inside the useSelectors and see them get fired off every time I click the button. minedDiamond should be the only value to change when I click the button.
Minecraft.js
import React, { useEffect, useCallback } from "react";
import { connect, useSelector, useDispatch, shallowEqual } from "react-redux";
import { mineDiamond, fetchMinecraftItems } from "../redux/diamonds/actions";
const Minecraft = () => {
const loading = useSelector((state) => {
console.log("loading output");
return state.diamond.loading;
});
let minedDiamond = useSelector((state) => {
console.log("diamond output");
return state.diamond.minedDiamond;
});
const names = useSelector((state) => {
console.log("name rendered");
let data = state.diamond.minecraftData;
return data.map((i) => i.name);
});
const dispatch = useDispatch();
const handleClick = () => dispatch(mineDiamond((minedDiamond += 1)));
useEffect(() => {
dispatch(fetchMinecraftItems());
}, []);
console.log({ loading });
return (
<div className="wrapper">
<div className="wrapper__item">
<img src="/image/pickaxe.png" alt="diamond" />
<button onClick={handleClick} type="button" className="wrapper__button">
Mine
<span role="img" aria-label="cart">
🛒
</span>
</button>
</div>
<div className="wrapper__item">
<img src="/image/diamond.png" alt="axe" />
<span className="num">{minedDiamond}</span>
</div>
<div className="num">
{loading ? (
<p>loading...</p>
) : (
<h1 className="num">{names}</h1>
)}
</div>
</div>
);
};
export default Minecraft;
Action Creators
import * as Actions from "./actionTypes";
import axios from "axios";
//action creator
export const mineDiamond = (addDiamond) => ({
type: Actions.MINE_DIAMOND,
payload: addDiamond,
});
export function fetchMinecraftItems() {
return function (dispatch) {
dispatch({ type: Actions.MINECRAFT_DATA_FETCH });
return fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((json) => {
dispatch({ type: Actions.MINECRAFT_DATA_SUCCESS, payload: json });
})
.catch((err) =>
dispatch({ type: Actions.MINECRAFT_DATA_FAIL, payload: err })
);
};
}
Reducer
import * as Actions from "./actionTypes";
//reducer holds initial state
const initialState = {
minedDiamond: 0,
minecraftData: [],
loading: false,
error: null,
};
//reducer
const diamondReducer = (state = initialState, action) => {
switch (action.type) {
case Actions.MINE_DIAMOND:
return {
...state,
minedDiamond: action.payload,
};
case Actions.MINECRAFT_DATA_FETCH:
return {
...state,
loading: true,
};
case Actions.MINECRAFT_DATA_SUCCESS:
return {
...state,
loading: false,
minecraftData: action.payload,
};
case Actions.MINECRAFT_DATA_FAIL:
return {
...state,
error: action.payload,
};
default:
return state;
}
};
export default diamondReducer;
Your issue has nothing to do with the usage of console.log.
Would you kindly include the code for your action creators and reducers?
Assuming state.diamond.mincraftdata is an array, the line
return data.map((i) => i.name);
creates a new array every time the selector is run. As per the react-redux docs:
when an action is dispatched to the Redux store, useSelector() only forces a re-render if the selector result appears to be different than the last result. As of v7.1.0-alpha.5, the default comparison is a strict === reference comparison.
I'm updating a project from a MERN stack course I completed earlier. It has a 'Like' and 'Dislike' button, which update in MongoDB, and then the updated number of likes gets rendered to the screen. Right now, it's updating the database, but not re-rendering the updated information on the screen.
For example, I'll hit the like button on a post (should take it from 2 likes to 3). I can see the updated information in MongoDB (3), but it never adds the like on the screen (2). Additionally, Redux DevTools tells me it didn't update the state automatically (still 2). I then hit the 'refresh' button on my browser, and the updated number of likes is there (3).
I used this component and these actions/reducers in another component, and they work perfectly. For some reason, it's behaving differently here. Maybe something to do with the props I'm passing in? UseEffect issue?
I tried console.logging the likes property (post.likes) before and after the PostLikeAndDislike component, and it's the same number of likes both times (that is, if it's 3 before I click, it's 3 after I click).
PostLikeAndDislike component:
const PostLikeAndDislike = ({
addLike,
removeLike,
deletePost,
auth,
post: { _id, text, name, avatar, user, likes, comments, date },
showActions
}) => (
<div className="post bg-white p-1 my-1">
<div>
{showActions && (
<Fragment>
<button
onClick={e => addLike(_id)}
type="button"
className="btn btn-light"
>
<i className="fas fa-thumbs-up" />{' '}
{likes.length > 0 && <span>{likes.length}</span>}
</button>
<button
onClick={e => removeLike(_id)}
type="button"
className="btn btn-light"
>
<i className="fas fa-thumbs-down" />
</button>
</Fragment>
)}
</div>
</div>
)
PostLikeAndDislike.defaultProps = {
showActions: true
}
const mapStateToProps = state => ({
auth: state.auth
})
export default connect(
mapStateToProps,
{ addLike, removeLike, deletePost }
)(PostLikeAndDislike)
The component PostLikeAndDislike is being imported into:
const Post = ({ getPost, post: { post, loading }, match }) => {
useEffect(
() => {
getPost(match.params.id)
},
[getPost]
)
return loading || post === null ? (
<Spinner />
) : (
<Fragment>
{console.log('Before', post.likes)}
<PostLikeAndDislike post={post} showActions={true} />
<div className="comments">
{post.comments.map(comment => (
<CommentItem key={comment._id} comment={comment} postId={post._id} />
))}
</div>
{console.log('After', post.likes)}
<CommentForm postId={post._id} />
</Fragment>
)
}
const mapStateToProps = state => ({
post: state.post
})
export default connect(
mapStateToProps,
{ getPost, addLike, removeLike }
)(Post)
Actions:
// Get post
export const getPost = id => async dispatch => {
try {
const res = await axios.get(`/api/posts/${id}`)
dispatch({
type: GET_POST,
payload: res.data
})
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
})
}
}
// Add like
export const addLike = id => async dispatch => {
try {
const res = await axios.put(`/api/posts/like/${id}`)
dispatch({
type: UPDATE_LIKES,
payload: { id, likes: res.data }
})
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
})
}
}
// Remove like
export const removeLike = id => async dispatch => {
try {
const res = await axios.put(`/api/posts/unlike/${id}`)
dispatch({
type: UPDATE_LIKES,
payload: { id, likes: res.data }
})
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
loading: false
})
}
}
Reducer:
const initialState = {
posts: [],
post: null,
loading: true,
error: {}
}
case GET_POST:
return {
...state,
post: payload,
loading: false
}
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(
post =>
post._id === payload.id ? { ...post, likes: payload.likes } : post
)
}
Like I said above, I'm expecting the 'like' button and 'dislike' button to change the number of likes and re-render it to the screen immediately.
Thanks in advance!