Delete element from array in redux state using a reducer in createSlice - javascript

I've been scratching my brain for a while now with this one and any help would be appreciated.
I am using React with Redux Toolkit and I'm struggling to get React to remove a 'todo' from my UI even though Redux is responding as expected. In Redux Developer Tools removeTodo works as expected, removing a todo from the todos array state, but React doesn't follow and therefore my UI doesn't change with it. My addTodo action works as expected in both React and Redux.
My current code provides me with the following error when I click the button that calls the dispatch of removeTodo.
TypeError: Cannot read property 'length' of undefined
App
C:/Users/joeee/Documents/redux-middleware/src/app/App.js:13
10 |
11 | return (
12 | <div style={divStyles}>
> 13 | <TodosForm />
| ^ 14 | {todos.length > 0 && <TodoList />}
15 | </div>
16 | )
View compiled
▶ 19 stack frames were collapsed.
It should be noted that I am only rendering in my TodoList component when my todos array state has a length > 0 as I don't want the component rendered in when there are no todos. I am new to React and Redux and there is probably a very simple solution but from what I can decipher is that when removeTodo is called, the todos array state is being removed completely rather than just returning those with id's not equal to the id passed in. This is why I assume the error I am getting is telling me it can't read the .length of undefined because my todos state is now empty.
I removed the requirement for the todos.length needing to be greater than 0 for TodoList to render but then I got the error that it couldn't read .map of undefined (my todos state) in TodoList which to me reinforces that my whole todos state seems to be being deleted.
Here is my todosSlice:
import { createSlice } from '#reduxjs/toolkit';
export const todosSlice = createSlice({
name: 'todos',
initialState: {
todos: [],
},
reducers: {
addTodo: (state, action) => {
const { id, task } = action.payload;
state.todos.push({ id, task })
},
removeTodo: (state, action) => {
// console.log(state.todos);
const { id } = action.payload;
// console.log(id);
return state.todos.filter(item => item.id !== id);
}
},
});
export const selectTodos = state => state.todos.todos;
export const { addTodo, removeTodo } = todosSlice.actions;
export default todosSlice.reducer;
App.js:
import React from 'react';
import { useSelector } from 'react-redux';
import TodosForm from '../components/TodosForm';
import TodoList from '../components/TodoList';
import { selectTodos } from '../features/todosSlice';
export const App = () => {
const todos = useSelector(selectTodos);
// console.log(todos.length);
return (
<div style={divStyles}>
<TodosForm />
{todos.length > 0 && <TodoList />}
</div>
)
}
export default App;
TodoList.js
import React from 'react';
import { useSelector } from 'react-redux';
import { selectTodos } from '../features/todosSlice';
import Todos from './Todos';
const TodoList = () => {
const todos = useSelector(selectTodos);
// console.log(todos);
return (
<div style={divStyles}>
<h3 style={headerStyles}>Your Todos: </h3>
{todos.map(todo => <Todos key={todo.id} task={todo.task} id={todo.id} />)}
</div>
)
}
export default TodoList
Todos.js
import React from 'react';
import { useDispatch } from 'react-redux';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faTrashAlt } from '#fortawesome/free-solid-svg-icons'
import { faEdit } from '#fortawesome/free-solid-svg-icons'
import { removeTodo } from '../features/todosSlice';
const Todos = ({ task, id }) => {
const dispatch = useDispatch();
const handleDeleteClick = () => {
dispatch(removeTodo({id: id}));
}
return (
<div style={divStyles}>
<li style={listStyles}>{task}</li>
<div>
<button className="faEditIcon" style={btnStyles}><FontAwesomeIcon icon={faEdit}/></button>
<button className="faDeleteIcon" style={btnStyles} onClick={handleDeleteClick}><FontAwesomeIcon icon={faTrashAlt}/></button>
</div>
</div>
)
}
export default Todos;
And my store.js
import { configureStore } from '#reduxjs/toolkit';
import todosSliceReducer from '../features/todosSlice';
export default configureStore({
reducer: {
todos: todosSliceReducer,
},
});

Can you update removeTodo as below and see.
removeTodo: (state, action) => {
// console.log(state.todos);
const { id } = action.payload;
// console.log(id);
state.todos = state.todos.filter(item => item.id !== id)
}

Related

React Redux & ContextApi - How to pass a prop via context and keeping it "connected"?

Considering the following project setup on a react-redux application that uses context API to avoid prop drilling. The example given is simplified.
Project Setup
React project uses React Redux
Uses context API to avoid prop drilling in certain cases.
Redux store has a prop posts which contains list of posts
An action creator deletePost(), which deletes a certain post by post id.
To avoid prop drilling, both posts and deletePosts() is added to a context AppContext and returned by a hook funciton useApp().
posts array is passed via contexts so it is not used by connect() function. Important
Problem:
When action is dispatched store is updated however Component is not re-rendered (because the prop is not connected?). Of course, if I pass the prop with connect function and drill it down to child rendering works fine.
What is the solution?
Example Project
The example project can be found in codesandbox. Open up the console and try to click the delete button. You will see no change in the UI while you can see the state is updated in the console.
Codes
App.js
import Home from "./routes/Home";
import "./styles.css";
import { AppProvider } from "./context";
export default function App() {
return (
<AppProvider>
<div className="App">
<Home />
</div>
</AppProvider>
);
}
context.js
import { useDispatch, useStore } from "react-redux";
import { useContext, createContext } from "react";
import { deletePost } from "./redux/actions/posts";
export const AppContext = createContext();
export const useApp = () => {
return useContext(AppContext);
};
export const AppProvider = ({ children }) => {
const dispatch = useDispatch();
const {
posts: { items: posts }
} = useStore().getState();
const value = {
// props
posts,
// actions
deletePost,
dispatch
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
Home.js
import { connect } from "react-redux";
import Post from "../components/Post";
import { useApp } from "../context";
const Home = () => {
const { posts } = useApp();
return (
<section>
{posts.map((p) => (
<Post key={p.id} {...p} />
))}
</section>
);
};
/*
const mapProps = ({ posts: { items: posts } }) => {
return {
posts
};
};
*/
export default connect()(Home);
Post.js
import { useApp } from "../context";
const Post = ({ title, content, id }) => {
const { deletePost, dispatch } = useApp();
const onDeleteClick = () => {
console.log("delete it", id);
dispatch(deletePost(id));
};
return (
<article>
<h1>{title}</h1>
<p>{content}</p>
<div className="toolbar">
<button onClick={onDeleteClick}>Delete</button>
</div>
</article>
);
};
export default Post;
You're not using the connect higher order component method properly . Try using it like this so your component will get the states and the function of your redux store :
import React from 'react';
import { connect } from 'react-redux';
import { callAction } from '../redux/actions.js';
const Home = (props) => {
return (
<div> {JSON.stringify(props)} </div>
)
}
const mapState = (state) => {
name : state.name // name is in intialState
}
const mapDispatch = (dispatch) => {
callAction : () => dispatch(callAction()) // callAction is a redux action
//and should be imported in the component also
}
export default connect(mapState , mapDispatch)(Home);
You can access the states and the actions from your redux store via component props.
Use useSelector() instead of useState(). Example codepen is fixed.
Change from:
const { posts: { items: posts } } = useStore().getState();
Change to:
const posts = useSelector(state => state.posts.items);
useStore() value is only received when component is first mounted. While useSlector() will get value when value is changed.

Cannot read property 'type' of undefined in Redux

i'm write react functional component, which should get some data from server. I'm use redux, but i get an error "Cannot read property 'type' of undefined"
Help me please find my mistake
This is my react component, Products.js. I think there may be errors in the export part. Also parent component Products.js (App.js) has wrapped on Provider
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { fetchProducts } from '../actions/productActions';
import { productsReducer } from '../reducers/productReducers'
function Products({ store, products, cartItems, setCartItems }) {
useEffect(() => {
store.dispatch(productsReducer())
})
const [product, setProduct] = useState(null);
return (
<div>
{
!products
? (<div>Loading...</div>)
:
(<ul className="products">
{products.map(product => (
<li key={product._id}>
<div className="product">
<a href={"#" + product._id}>
<img src={product.image} alt={product.title} />
<p>{product.title}</p>
</a>
<div className="product-price">
<div>${product.price}</div>
<button>Add To Cart</button>
</div>
</div>
</li>
))}
</ul>)
}
</div >
)
}
export default connect((state) => ({ products: state.products.items }), {
fetchProducts,
})(Products);
This is my store.js
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { productsReducer } from './reducers/productReducers';
const initialState = {};
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
combineReducers({
products: productsReducer,
}),
initialState,
composeEnhancer(applyMiddleware(thunk))
)
export default store;
My reducer, and I am getting error in this file (Cannot read property 'type' of undefined), maybe, I making a transmission error 'action'
import { FETCH_PRODUCTS } from "../types";
export const productsReducer = (state = {}, action) => {
switch (action.type) {
case FETCH_PRODUCTS:
console.log('it work!')
return { items: action.payload };
default:
return state;
}
};
My Action
import { FETCH_PRODUCTS } from "../types";
export const fetchProducts = () => async (dispatch) => {
const res = await fetch("/api/products");
const data = await res.json();
console.log(data);
dispatch({
type: FETCH_PRODUCTS,
payload: data,
});
};
and my Types
export const FETCH_PRODUCTS = "FETCH_PRODUCTS"
This is are study project on redux, but in original teacher writte code on class component. Original code here and here
If i write class component like on source, everything is working, so i think this is a reason of mistake
dispatch function expects redux action as an argument, not reducer. In your case:
import { fetchProducts } from '../actions/productActions';
function Products({ store, products, cartItems, setCartItems }) {
useEffect(() => {
store.dispatch(fetchProducts());
})
...
}

props undefined despite setting it in the redux store

I get a strange error that concerns a reducer named prs for a given added or deleted person, in a nutshell this app allows to add or remove a person when clicked. each person has a random id.
First of all that's my parent component App.jsx:
import React, { Component } from 'react';
import PersonsComponent from './containers/PersonsComponent';
class App extends Component {
render() {
return (
<div className="App">
<ol>
<li>Turn this app into one which does NOT use local state (in components) but instead uses Redux</li>
</ol>
<PersonsComponent />
</div>
);
}
}
export default App;
The person component (PersonsComponent.jsx) is depicted as below:
import React, { Component } from "react";
import Person from "../components/Person/Person";
import AddPerson from "../components/AddPerson/AddPerson";
class PersonsComponent extends Component {
render() {
const { prs, personAddedHandler, personDeletedHandler } = this.props;
return (
<div>
<AddPerson personAdded={() => personAddedHandler(prs)} />
{prs.map(person => (
<Person
key={person.id}
name={person.name}
age={person.age}
clicked={() => personDeletedHandler(prs, person.id)}
/>
))}
</div>
);
}
}
export default PersonsComponent;
The PersonComponent container (PersonsContainer.jsx) that holds its props such as the prs reducer which means a given person, and the dispatchers actions personAddedHandler and personDeletedHandler:
import actions from './actions';
import { connect } from "react-redux";
import PersonsComponent from './PersonsComponent';
const mapStateToProps = state => {
return {
prs: state.persons
};
};
const mapDispatchToProps = dispatch => {
return {
personAddedHandler: persons => dispatch(actions.addPerson(persons)),
personDeletedHandler: (persons, personId) =>
dispatch(actions.deletePerson(persons, personId))
};
};
const PersonsContainer = connect(
mapStateToProps,
mapDispatchToProps
)(PersonsComponent);
export default PersonsContainer;
Below our headache reducer (reducer.jsx):
import types from "../constants/types";
const initialState = {
persons: [
{
id: Math.random(),
name: "Max",
age: Math.floor(Math.random() * 40)
}
]
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case types.ADD_PERSON.type:
return {
...state,
persons: action.payload
};
case types.DELETE_PERSON.type:
return {
...state,
persons: action.payload
};
default:
return state;
}
};
export default reducer;
And of course each reducer has its own action:
import types from "../constants/types";
export default {
addPerson: (persons) => {
const newPerson = {
id: Math.random(), // not really unique but good enough here!
name: "Max",
age: Math.floor(Math.random() * 40)
};
return {
type: types.ADD_PERSON.type,
payload: persons.concat(newPerson)
};
},
deletePerson: (persons, personId) => ({
type: types.DELETE_PERSON.type,
payload: persons.filter(person => person.id != personId)
})
};
But when I run my app with npm start I get the following error as screened below:
This error tells me that the store is likely not known (just a doubt, I'm not sure).
The redux store is still defined in index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import reducer from './store/reducer';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const store = createStore(reducer);
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
registerServiceWorker();
Any help will be appreciated, thanks for your answer.
The problem is tat you are using the PersonComponent, that is not the wrapped one and does not have the props coming from Redux. Use the PersonsContainer insetead in you App.js file.
Usually this happens when the variable is undefined and you are calling map() on that. The variable has to be an array, even if it is an empty one.
In React what you can do is using && to prevent that error what you have.
Please find a possible solution:
{prs && prs.map(person => (
<Person
key={person.id}
name={person.name}
age={person.age}
clicked={() => personDeletedHandler(prs, person.id)}
/>
))}
In this way you are checking if prs does have a value and if it has then runs map() on that.
I hope that helps!
Please add condition to check null and length before binding
{prs && prs.length > 0 && (prs.map(person => (
<Person
key={person.id}
name={person.name}
age={person.age}
clicked={() => personDeletedHandler(prs, person.id)}
/>
)))}

"TypeError: dispatch is not a function" when using useReducer/useContext and React-Testing-Library

I'm having issues testing my components that use dispatch via useReducer with React-testing-library.
I created a less complex example to try to boil down what is going on and that is still having the same dispatch is not a function problem. When I run my tests, I am getting this error:
11 | data-testid="jared-test-button"
12 | onClick={() => {
> 13 | dispatch({ type: 'SWITCH' })
| ^
14 | }}
15 | >
16 | Click Me
Also, if I do a console.log(typeof dispatch) inside RandomButton, and I click on the button the output says function.
Here is the test in question.
import React from 'react'
import RandomButton from '../RandomButton'
import { render, fireEvent } from '#testing-library/react'
describe('Button Render', () => {
it('click button', () => {
const { getByTestId, queryByText } = render(<RandomButton />)
expect(getByTestId('jared-test-button')).toBeInTheDocument()
fireEvent.click(getByTestId('jared-test-button'))
expect(queryByText('My name is frog')).toBeInTheDocument()
})
})
Here is my relevant code:
RandomButton.js
import React, { useContext } from 'react'
import MyContext from 'contexts/MyContext'
const RandomButton = () => {
const { dispatch } = useContext(MyContext)
return (
<div>
<Button
data-testid="jared-test-button"
onClick={() => {
dispatch({ type: 'SWITCH' })
}}
>
Click Me
</Button>
</div>
)
}
export default RandomButton
MyApp.js
import React, { useReducer } from 'react'
import {myreducer} from './MyFunctions'
import MyContext from 'contexts/MyContext'
import RandomButton from './RandomButton'
const initialState = {
blue: false,
}
const [{ blue },dispatch] = useReducer(myreducer, initialState)
return (
<MyContext.Provider value={{ dispatch }}>
<div>
{blue && <div>My name is frog</div>}
<RandomButton />
</div>
</MyContext.Provider>
)
export default MyApp
MyFunctions.js
export const myreducer = (state, action) => {
switch (action.type) {
case 'SWITCH':
return { ...state, blue: !state.blue }
default:
return state
}
}
MyContext.js
import React from 'react'
const MyContext = React.createContext({})
export default MyContext
It is probably something stupid that I am missing, but after reading the docs and looking at other examples online I'm not seeing the solution.
I've not tested redux hooks with react-testing-library, but I do know you'll have to provide a wrapper to the render function that provides the Provider with dispatch function.
Here's an example I use to test components connected to a redux store:
testUtils.js
import React from 'react';
import { createStore } from 'redux';
import { render } from '#testing-library/react';
import { Provider } from 'react-redux';
import reducer from '../reducers';
// https://testing-library.com/docs/example-react-redux
export const renderWithRedux = (
ui,
{ initialState, store = createStore(reducer, initialState) } = {},
options,
) => ({
...render(<Provider store={store}>{ui}</Provider>, options),
store,
});
So, based upon what you've shared I think the wrapper you'd want would look something like this:
import React from 'react';
import MyContext from 'contexts/MyContext';
// export so you can test that it was called with specific arguments
export dispatchMock = jest.fn();
export ProviderWrapper = ({ children }) => (
// place your mock dispatch function in the provider
<MyContext.Provider value={{ dispatch: dispatchMock }}>
{children}
</MyContext.Provider>
);
and in your test:
import React from 'react';
import RandomButton from '../RandomButton';
import { render, fireEvent } from '#testing-library/react';
import { ProviderWrapper, dispatchMock } from './testUtils';
describe('Button Render', () => {
it('click button', () => {
const { getByTestId, queryByText } = render(
<RandomButton />,
{ wrapper: ProviderWrapper }, // Specify your wrapper here
);
expect(getByTestId('jared-test-button')).toBeInTheDocument();
fireEvent.click(getByTestId('jared-test-button'));
// expect(queryByText('My name is frog')).toBeInTheDocument(); // won't work since this text is part of the parent component
// If you wanted to test that the dispatch was called correctly
expect(dispatchMock).toHaveBeenCalledWith({ type: 'SWITCH' });
})
})
Like I said, I've not had to specifically test redux hooks but I believe this should get you to a good place.

Can´t access props after using CombineReducer

My previous React-Redux implementation was working, but after I tried to implement the combineReducer function with seperated files, an error is thrown that I don´t really understand. Hope some of you can help me!
ERROR: Uncaught TypeError: this.props.todos.map is not a function
My Reference for that Code was the Async Example of the Redux-Doc´s. But I stated with another example and the change from each examples are not documented in the doc´s.
The first code I will show, is that I had (working):
MyStore
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import addItem from '../reducers/addItem'
export default function configureStore(preloadedState) {
const store = createStore(
addItem,
preloadedState,
applyMiddleware(thunkMiddleware, createLogger())
)
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextRootReducer = require('../reducers').default
store.replaceReducer(nextRootReducer)
})
}
return store
}
My Reducer
export default (state = ['Test'], action) => {
switch (action.type){
case 'ADD_ITEM':
//return action.item
return [
...state,
{
id: action.id,
text: action.item
}
]
default:
return state
}
}
Actions
export default function addItem(item){
console.log("addTOdo")
return {
type: 'ADD_ITEM',
id: nextTodoId++,
item
}
}
And the subComponent where the input is finally rendered
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
export default class TodoList extends Component {
render() {
const posts = this.props
const isEmpty = posts.length === 0
return (
<div>
<h3>Meine Aufgaben</h3>
<ul>
{isEmpty
? <h3>Sie haben noch keinen Todo´s angelegt</h3>
: <h3>Ihre Ergebnisse</h3>
}
{this.props.todos.map((todo, i) => <li key={i}>{todo.text} </li>)}
</ul>
</div>
)
}
}
const mapStateToProp = state => ({todos: state})
export default connect (mapStateToProp)(TodoList)
What I have change:
First, I created another Reducers File, called Index where I imported the addItem Reducer and exported the rootReducer:
import {combineReducers} from 'redux'
import addItem from './addItem'
import getItem from './getItem'
const rootReducer = combineReducers({
addItem,
getItem
})
export default rootReducer
After that, I changed the Store to import the rootReducer and put it´s reference in the Store (just the changes to configureStore):
import rootReducer from '../reducers/index'
const store = createStore(
rootReducer,
preloadedState,
applyMiddleware(thunkMiddleware, createLogger())
)
I don´t know if that Information is also required, but here is my Container Component:
import React, { Component, PropTypes } from 'react'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import { connect } from 'react-redux'
import addItem from '../actions/addItem'
import getItems from '../actions/getItems'
class App extends Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
this.state = {text: ''}
}
handleClick(e){
console.log(e);
const {dispatch} = this.props
dispatch(addItem(e));
}
componentDidMount(){
console.log("COMPONENT MOUNT");
const {dispatch} = this.props
// dispatch(getItems())
}
componentWillReceiveProps(nextProps) {
console.log("GETTT IT");
console.log(nextProps)
}
render() {
return (
<div>
< h1 > Hallo </h1>
<AddTodo handleAddItem={this.handleClick}/>
<TodoList/>
</div>
)
}
}
App.propTypes = {
dispatch: PropTypes.func.isRequired
}
function mapStateToProps(state){
return {
AddTodo
}
}
export default connect (mapStateToProps)(App)
I hope this issue is not to basic and someone can help me. Thanks in advance!
If you inspect your redux state you will see that the following code sets up 2 more keys in the state (addItem and getItem):
const rootReducer = combineReducers({
addItem,
getItem
})
So, now to connect todos you need to one of the 2 new keys. If todos is not defined on those, then you need to add the reducer of todos to the combineReducers call.
So this needs to map to a valid location in state:
const mapStateToProp = state => ({todos: state})

Categories