Okies, So, I am working on a simple recipe collection App, where I have cuisineReducer.js and RecipeReducer.js.
and on my admin Dashboard, an admin user is allowed to Create, Delete and Edit any Cuisine groups and also could Create, Delete and Edit Recipes for any Cuisine Group.
when user clicks on any cuisine group, it renders all the Recipes in Recipe Panel using Cuisine Id. user can also delete by pressing "x" on Recipe or on Cuisine group.
The conflict I need to resolve is when a recipe is deleted,it should be deleted from the ui too without the need of refresh. I dont know how to achieve that. Here is my code.
CuisineReducer.js
import { GET_CUISINES, GET_CUISINE_BY_ID, GET_CUISINE_BY_CATEGORY} from "../actions/types.js";
const initialState = {
cuisines: [],
cuisine:{}
};
export default function (state = initialState, action) {
switch(action.type) {
case GET_CUISINES:
return {
...state,
cuisines:action.payload
};
case GET_CUISINE_BY_CATEGORY:
return {
...state,
cuisines:action.payload
};
case GET_CUISINE_BY_ID:
return {
...state,
cuisine:action.payload
};
default:
return state;
}
CuisineActions.js
import { GET_CUISINES, GET_CUISINE_BY_ID} from "./types.js";
import axios from 'axios';
export const getCuisines = () =>async dispatch => { ... }
export const getCuisineByCategory = (id) =>async dispatch => {
const res = await axios.get(`/api/cuisine/${id})
dispatch({
type:GET_CUISINES_BY_CATEGORY,
payload: res.data
});
export const getCuisineById = (id) =>async dispatch => {
const res = await axios.get(`/api/cuisine/${id})
dispatch({
type:GET_CUISINE_BY_ID,
payload: res.data
});
}
recipeReducer.js
import { GET_RECIPES, GET_RECIPE_BY_ID, DELETE_RECIPE} from "../actions/types.js";
const initialState = {
recipes: [],
recipe:{}
};
export default function (state = initialState, action) {
switch(action.type) {
case GET_RECIPES:
return {
...state,
recipes:action.payload
};
case GET_RECIPE_BY_ID:
return {
...state,
recipe:action.payload
};
case DELETE_RECIPE:
return {
...state,
recipes:state.recipes.filter(asset => asset.id != action.payload)
};
default:
return state;
}
RecipeActions.js
import { DELETE_RECIPE} from "./types.js";
import axios from 'axios';
export const getRecipes = () =>async dispatch => { ... }
export const deleteRecipe = (id) =>async dispatch => {
await axios.delete(`/api/recipe/${id})
dispatch({
type:GET_RECIPE_BY_ID,
payload: id
});
}
Admin.js
import React, {Component} from react;
import {connect} from 'react-redux';
import { getCuisineById, getCuisinesByCategory} from '../../actions/cuisineAction.js';
import AdminCuisineList from '../AdminCuisineList.js;
import AdminRecipeList from '../AdminRecipeList.js";
Class Admin extends Component {
componentWillUnmount = () => {
this.props.clearCuisine();
}
revealList = category => {
this.props.getCuisinesByCategory(caegory);
}
revealRecipeList = id => {
this.props.getCuisineById(id);
}
render () {
const {cuisines} = this.props;
return(
<div>
<div>
<ul>
<li onClick={() => revealList('meal')}> Meal </li>
<li onClick={() => revealList('meal')}> Deserts </li>
<li onClick={() => revealList('meal')}> Drinks </li>
</ul>
</div>
{ cuisines &&
cuisines.map(cuisine => (
<AdminCuisineList label={cuisine.label} recipeHandler={() => this.revealRecipeList(cuisine.id)}} />
))}
{ cuisine && cuisine.recipes
cuisine.recipes.map(recipe => (
<AdminRecipeList label=recipe.label} cuisineId={recipe.cuisine[0] />
))}
);
}
}
Here is my json data for the calls I am using:
getCuisinesByCategory() GET Call
Output:
[
"label": "Indian",
"category": "Meal",
"recipes": [
{ id: "123", "label": "Chicken Biryani"},
{ id: "124", "label": "Chicken Korma"},
{ id: "125", "label: "Nihari"},
{ id: "1244", "label": "Pulao"},
{ id: "12321", "label": Pakore"},
{ id: "1232", "label": "Bun Kubab"}
]
]
It looks like you handle deleting recipes in your reducer - you are using the wrong action for when you delete your recipe. You need to use your DELETE_RECIPE action type. You are using GET_RECIPE_BY_ID
export const deleteRecipe = (id) =>async dispatch => {
await axios.delete(`/api/recipe/${id})
dispatch({
type:DELETE_RECIPE,
payload: id
});
}
Related
When I use dispatch as follows in my react component, My component keeps rendering. How can I avoid that?
const dispatch = useDispatch();
useEffect(() => {
dispatch(reportsActionCreators.changeSalesDashboardData(someData));
}, []);
in the parent component, I'm using useSelector as this. But didn't use this report's data.
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
this is the parent component I'm using.
const SalesReports: FC = () => {
const dispatch = useDispatch();
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
});
const getPageContent = useMemo(() => {
switch (selectedSalesTab) {
case salesReportsTabs[0].id:
return <Dashboard />;
default:
return <div>not found :(</div>;
}
}, [selectedSalesTab]);
return (
<div className="sales-report-wrapper">
<GTTabs
id="sales-reports-tabs"
onClickTab={(tab: Tab) => dispatch(reportsActionCreators.changeSalesTab(tab.id))}
tabs={salesReportsTabs}
defaultSelectedTabId={selectedSalesTab}
/>
<div>{getPageContent}</div>
</div>
);
};
export default SalesReports;
this is the Child component I'm using
const Dashboard: FC = () => {
const repostsRxjs = rxjsConfig(reportingAxios);
const dispatch = useDispatch();
useEffect(() => {
repostsRxjs
.post<SalesDashboardItem[]>(
'/sales-data/order-details/6087bc3606ff073930a10848?timezone=Asia/Dubai&from=2022-09-03T00:00:00.00Z&to=2022-12-25T00:00:00.00Z&size=10',
{
brandIds: [],
channelIds: [],
kitchenIds: [],
countryIds: [],
},
)
.pipe(
take(1),
catchError((err: any) => of(console.log(err))),
)
.subscribe((response: SalesDashboardItem[] | void) => {
if (response) {
dispatch(reportsActionCreators.changeSalesDashboardData(response));
}
});
}, []);
const { isActiveFilter } = useSelector<RootState, any>((state: RootState) => {
return {
isActiveFilter: state.filterData.isActiveFilter,
};
});
return (
<>
<div
onClick={() => {
dispatch(filterssActionCreators.handleFilterPanel(!isActiveFilter));
dispatch(
filterssActionCreators.changeSelectedFiltersType([
FilterTypes.BRAND,
FilterTypes.CHANNEL,
FilterTypes.COUNTRY,
FilterTypes.KITCHEN,
]),
);
}}
>
Dashboard
</div>
{isActiveFilter && <FilterPanel />}
</>
);
};
export default Dashboard;
Actions
import { SalesDashboardItem } from 'app/models/Reports';
import { actionCreator } from 'app/state/common';
export type ChangeSalesTabPayload = string;
export type ChangeSalesDashboardDataPayload = SalesDashboardItem[];
export const reportsActionTypes = {
CHANGE_SALES_TAB: 'CHANGE_SALES_TAB',
CHANGE_SALES_DASHABOARD_DATA: 'CHANGE_SALES_DASHABOARD_DATA',
};
export const reportsActionCreators = {
changeSalesTab: actionCreator<ChangeSalesTabPayload>(reportsActionTypes.CHANGE_SALES_TAB),
changeSalesDashboardData: actionCreator<ChangeSalesDashboardDataPayload>(
reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA,
),
};
export type ReportsAction = {
type: typeof reportsActionTypes.CHANGE_SALES_TAB | typeof reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA;
payload: ChangeSalesTabPayload | ChangeSalesDashboardDataPayload;
};
Reducer
import { SalesDashboardItem } from 'app/models/Reports';
import { salesReportsTabs } from 'app/utils/reports';
import { reportsActionTypes, ReportsAction } from './actions';
export type ReportsState = {
selectedSalesTab: string;
salesDashboardFilterData: {
brands: string[];
kitchens: string[];
channels: string[];
countries: string[];
};
salesDashBoardDatta: SalesDashboardItem[];
};
const initialState: ReportsState = {
selectedSalesTab: salesReportsTabs[0].id,
salesDashboardFilterData: {
brands: [],
kitchens: [],
channels: [],
countries: [],
},
salesDashBoardDatta: [],
};
export default (state = initialState, action: ReportsAction): ReportsState => {
switch (action.type) {
case reportsActionTypes.CHANGE_SALES_TAB:
return { ...state, selectedSalesTab: action.payload as string };
case reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA:
return { ...state, salesDashBoardDatta: action.payload as SalesDashboardItem[] };
default:
return state;
}
};
root reducer
import { combineReducers } from 'redux';
import SidePanelReducer from './reducers/sidepanel.reducer';
import authReducer from './auth';
import onboardingReducer from './onboarding';
import applicationReducer from './application';
import inventoryConfigReducer from './inventoryConfig/inventory.reducer';
import reportsReducer from './reports';
import filtersReducer from './filter';
const rootReducer = combineReducers({
sidePanel: SidePanelReducer,
auth: authReducer,
onboarding: onboardingReducer,
application: applicationReducer,
inventory: inventoryConfigReducer,
reports: reportsReducer,
filterData: filtersReducer,
});
export default rootReducer;
when I'm adding the dispatch action in useEffect(componentDidMount) this looping is happening. Otherwise, this code works fine. How can I avoid that component rerendering?
I think the issue is that the useSelector hook is returning a new object reference each time which triggers the useMemo hook to re-memoize an "instance" of the Dashboard component. The new "instance" of Dashboard then mounts and runs its useEffect hook which dispatches an action that updates the state.reports state in the Redux store.
Instead of creating and returning a new object reference to destructure selectedSalesTab from, just return the state.reports object directly.
Change
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
});
to
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return state.reports;
});
I am passing value into the redux store through reducer. And I am displaying that value in the component.
But it says cannot read property name of undefined
Even whats weird is when I map the product, I can see product value in the console and when I don't map, I don't see the product value in the console. Please help me with this
Please find the code here
Component
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { listProductDetails } from "./actions/productActions";
const Playground = () => {
const dispatch = useDispatch();
const productDetails = useSelector((state) => state.productDetails);
const { product } = productDetails;
useEffect(() => {
dispatch(listProductDetails("pod2"));
}, [dispatch]);
return (
<div>
<h1>
<h1>{product[0].name}</h1>
</h1>
</div>
);
};
export default Playground;
Reducer
export const productDetailsReducers = (state = { product: [] }, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return { loading: true, ...state };
case PRODUCT_DETAILS_SUCCESS:
return {
loading: false,
product: action.payload,
};
case PRODUCT_DETAILS_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
Action
export const listProductDetails = (id) => async (dispatch) => {
try {
dispatch({ type: PRODUCT_DETAILS_REQUEST });
const { data } = await axios.get(`/api/products/${id}`);
console.log("this is the data");
console.log(data);
dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data });
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
Store
const reducer = combineReducers({
productDetails: productDetailsReducers,
});
It's always better to have a condition before accessing the data
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { listProductDetails } from "./actions/productActions";
const Playground = () => {
const dispatch = useDispatch();
const productDetails = useSelector((state) => state.productDetails);
const { product } = productDetails;
useEffect(() => {
dispatch(listProductDetails("pod2"));
}, [dispatch]);
return (
<div>
{
product &&
product.length ?
<h1>
<h1>{product[0].name || ""}</h1>
</h1>
: null
}
</div>
);
};
export default Playground;
I have created a simple blog app using react and redux, right now only two functionalities are present in the app -> rendering and deleting blogs. Rendering is working fine but not the delete one.
Most likely, the issue will be in the reducer of my application. Please Suggest.
Here's my whole code.
sandbox link: https://codesandbox.io/s/testing-75tjd?file=/src/index.js
ACTION
import { DELETE_BLOGS, GET_BLOGS } from "./actionTypes";
// for rendering list of blogs
export const getBlog = () => {
return {
type: GET_BLOGS,
};
};
// for deleting the blogs
export const deleteBlogs = (id) => {
return {
type: DELETE_BLOGS,
id,
};
};
REDUCER
import { DELETE_BLOGS, GET_BLOGS } from "../actions/actionTypes";
const initialState = {
blogs: [
{ id: 1, title: "First Blog", content: "A new blog" },
{ id: 2, title: "Second Blog", content: "Just another Blog" },
],
};
const blogReducer = (state = initialState, action) => {
switch (action.types) {
case GET_BLOGS:
return {
...state, // a copy of state
};
case DELETE_BLOGS:
return {
...state,
blogs: state.filter((blog) => blog.id !== action.id),
};
default:
return state; // original state
}
};
export default blogReducer;
COMPONENT
import React, { Component } from "react";
import { connect } from "react-redux";
import { deleteBlogs } from "../actions/blogActions";
class AllBlogs extends Component {
removeBlogs = (id) => {
console.log("removeBlogs function is running with id", id);
this.props.deleteBlogs(id); // delete action
};
render() {
return (
<div>
{this.props.blogs.map((blog) => (
<div key={blog.id}>
<h3>{blog.title}</h3>
<p>{blog.content}</p>
<button onClick={() => this.removeBlogs(blog.id)}>delete</button>
<hr />
</div>
))}
</div>
);
}
}
const mapStateToProps = (state) => ({
blogs: state.blogs,
});
export default connect(mapStateToProps, { deleteBlogs })(AllBlogs);
ISSUE
You were sending action key type and receiving as action.types in reducer
SOLUTION
const blogReducer = (state = initialState, action) => {
switch (action.type) { // change `types` to `type`
case DELETE_BLOGS:
return {
...state,
blogs: state.blogs.filter((blog) => blog.id !== action.id)
};
default:
return state; // original state
}
};
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;
}),
};
This is my component:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Divider } from "antd";
import MovieList from "../components/MovieList";
import IncreaseCountButton from "../components/IncreaseCountButton";
import DeleteButton from "../components/DeleteButton";
import { deleteMovie, increaseCount } from "../actions/movies";
import { getIsDeleting, getIsIncreasing } from "../reducers/actions";
export class MovieListContainer extends Component {
constructor(props) {
super(props);
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
static propTypes = {
isIncreasing: PropTypes.func.isRequired,
isDeleting: PropTypes.func.isRequired,
};
async handleIncrease(movie) {
await this.props.increaseCount(movie, this.props.token);
}
async handleDelete(movie) {
await this.props.deleteMovie(movie.id, this.props.token);
}
render() {
return (
<MovieList movies={this.props.movies}>
{(text, movie) => (
<div>
<IncreaseCountButton
onIncrease={() => this.handleIncrease(movie)}
loading={this.props.isIncreasing(movie.id)}
/>
<Divider type="vertical" />
<DeleteButton
onDelete={() => this.handleDelete(movie)}
loading={this.props.isDeleting(movie.id)}
/>
</div>
)}
</MovieList>
);
}
}
export const mapStateToProps = state => ({
isIncreasing: id => getIsIncreasing(state, id),
isDeleting: id => getIsDeleting(state, id),
});
export default connect(
mapStateToProps,
{ deleteMovie, increaseCount }
)(MovieListContainer);
I feel like this might be bad for performance/reconciliation reasons, but not sure how else to retrieve the state in a way that hides implementation details.
Gist link: https://gist.github.com/vitalicwow/140c06a52dd9e2e062b2917f5c741727
Any help is appreciated.
Here is how you can handle these asynchronous actions with redux. You can use thunk to perform 2 actions and can store a flag to determine what is being done to an object (Deleting, Changing, etc):
action
export const deleteMovieAction = id => {
return dispatch => {
dispatch({ type: "MOVIE_DELETING", id });
setTimeout(() => {
dispatch({ type: "MOVIE_DELETED", id });
}, 2000);
};
};
reducer
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "MOVIE_DELETING": {
const movies = [...state.movies];
movies.find(x => x.id === action.id).isDeleting = true;
return { ...state, movies };
}
case "MOVIE_DELETED": {
const movies = state.movies.filter(x => x.id !== action.id);
return { ...state, movies };
}
default:
return state;
}
};
https://codesandbox.io/s/k3jnv01ymv
An alternative is to separate out the ids into a new array that are being deleted
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "MOVIE_DELETING": {
const movieDeletingIds = [...state.movieDeletingIds, action.id];
return { ...state, movieDeletingIds };
}
case "MOVIE_DELETED": {
const movieDeletingIds = state.movieDeletingIds.filter(
x => x.id !== action.id
);
const movies = state.movies.filter(x => x.id !== action.id);
return { ...state, movieDeletingIds, movies };
}
default:
return state;
}
};
https://codesandbox.io/s/mj52w4y3zj
(This code should be cleaned up, but is just to demo using thunk)