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
}
};
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 created an action creator that is simply supposed to make a get request to my API and return with a list of all projects. However, for some reason, my return dispatch in my thunk function is not firing at all. It gets to the console.log() statement and just ends. There are no consoles errors, and no network calls being made either as far as I can tell. Any ideas why it would do absolutely nothing?
Dashboard.js (component)
import ProjectItem from "../Project/ProjectItem";
import styles from "./Dashboard.module.css";
import CreateProjectButton from "../CreateProjectButton/CreateProjectButton";
import { connect } from "react-redux";
import { getProjects } from "../../Redux/getProjects/actions";
const Dashboard = props => {
useEffect(() => {
console.log("blah");
getProjects();
}, []);
return (
<div className={styles.dashboardContainer}>
<h1>Projects</h1>
<br />
<CreateProjectButton />
<br />
<hr />
<ProjectItem />
</div>
);
};
const mapStateToProps = state => {
return {
projects: state
};
};
const mapDispatchToProps = dispatch => {
return {
getProjects: () => dispatch(getProjects())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
action.js (action creator)
import { GET_PROJECTS_SUCCESS, GET_PROJECTS_ERROR } from "./constants";
export const getProjectsSuccess = payload => {
console.log("getProjectSuccess", payload);
return {
type: GET_PROJECTS_SUCCESS,
payload
};
};
export const getProjectsError = () => {
console.log("there was an error");
return {
type: GET_PROJECTS_ERROR
};
};
export function getProjects() {
console.log("getProject");
return dispatch => {
axios
.get("/project/all")
.then(res => dispatch(getProjectsSuccess(res.data)))
.catch(err => dispatch(getProjectsError(err)));
};
}
index.js (getProject reducer)
const initialState = {
projects: [], //array of projects
project: {}, // single project for update case
reRender: false
};
const getProjectsReducer = (state = initialState, action) => {
switch (action.type) {
case GET_PROJECTS_SUCCESS:
return { ...state, projects: action.payload }; // will need to change action.payload later on
default:
return state;
}
};
export default getProjectsReducer;
constants.js
export const GET_PROJECTS_SUCCESS = "GET_PROJECTS_SUCCESS";
export const GET_PROJECTS_ERROR = "GET_PROJECTS_ERROR";
rootReducer.js
import createProjectReducer from "./createProject/index";
import getProjectsReducer from "./getProjects/index";
const rootReducer = (state = {}, action) => {
return {
project: createProjectReducer(state.project, action),
projects: getProjectsReducer(state.projects, action)
};
};
export default rootReducer;
FIXED: After reading up on the use effect hook in functional components I realized I was missing props.getProjects in the useEffect function in dashboard.js!
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
});
}
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)
I'm using this package https://github.com/RealScout/redux-infinite-scroll to make infinite scroll on list of brand. Here is my code:
Container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions, getBrands } from '../reducer';
import Infinite from 'react-infinite';
import InfiniteScroll from 'redux-infinite-scroll';
import SearchBox from '../components/SearchBox';
import CardList from '../components/CardList';
const { fetchBrands } = actions;
class BrandList extends Component {
componentDidMount() {
this.props.fetchBrands({ page: 1 });
}
renderList() {
const brands = this.props.brands;
return brands.map((brand) => {
return (
<CardList key={brand.id} name={brand.name} avatar={brand.avatar.thumbnail} follower={brand.follows_count} />
);
});
}
toggle() {
return this.props.isFetching;
}
loadMore() {
const {lastPage, currentPage} = this.props;
const nextPage = currentPage ? parseInt(currentPage) + 1 : 1;
if(currentPage && currentPage <= lastPage){
this.props.fetchBrands({page: nextPage});
}
}
render() {
return (
<div>
<SearchBox />
<div className="row">
<InfiniteScroll
items={this.renderList()}
loadMore={this.loadMore.bind(this)}
/>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
brands: getBrands(state),
isFetching: state.brand.isFetching,
currentPage: state.brand.currentPage,
lastPage: state.brand.lastPage
};
}
export default connect(mapStateToProps, { fetchBrands })(BrandList);
Reducer:
import axios from 'axios';
// Define Types
export const types = {
// brand list
FETCH_BRANDS: 'fetch_brands',
FETCH_BRANDS_SUCCESS: 'fetch_brands_success',
FETCH_BRANDS_ERROR: 'fetch_brands_failure',
FETCH_BRAND: 'fetch_brand',
FETCH_BRAND_SUCCESS: 'fetch_brand_success',
FETCH_BRAND_ERROR: 'fetch_brand_failure',
};
const { FETCH_BRANDS, FETCH_BRANDS_SUCCESS, FETCH_BRANDS_ERROR } = types;
// Define Reducer
export const INITIAL_STATE = { brands: [], brand: {}, isFetching: false, error: null, currentPage: 1 };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_BRANDS:
return { ...state, isFetching: true };
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: action.payload.brands.data, currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
case FETCH_BRANDS_ERROR:
return { ...state, error: action.payload };
default:
return state;
}
}
// Define Actions
export const actions = {
fetchBrands: ({page, count = 15}) => {
return (dispatch) => {
dispatch({ type: FETCH_BRANDS });
axios.get(`brands?page=${page}&count=${count}`)
.then((response) => {
const {data} = response;
if (data.code == 200) {
dispatch({ type: FETCH_BRANDS_SUCCESS, payload: data });
}
});
};
}
};
// SELECTOR
export const getBrands = (state) => state.brand.brands;
it run loadMore function successfully but it not extend current list, it replace it instead.
loadmore function only run once. it should run 10 times.
do I miss something on my code to make it scroll?
Try adding
brands: [ ...state.brands, ...action.payload.brands.data]
like this in your reducer
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: [ ...state.brands, ...action.payload.brands.data], currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
Which means that you are concating current list with upcoming list (versioned data)