Access dispatch function as a prop in the component - javascript

import React from "react";
import "./cart-dropdown.style.scss";
import { CustomButton } from "../cutom-button/custom-button.component";
import { connect } from "react-redux";
import { StoreState } from "../../redux/root-reducer";
import { ItemModel } from "../../models/ShopPage";
import { CartItem } from "../cart-item/cart-item.component";
import { selectCartItems } from "../../redux/cart/cart.selector";
import { createStructuredSelector } from "reselect";
import { withRouter, RouteComponentProps } from "react-router-dom";
interface CartDropdownStoreProps {
cartItems: ItemModel[];
}
interface CartDropdownProps extends CartDropdownStoreProps {}
const _CartDropdown: React.FC<CartDropdownProps & RouteComponentProps<{}>> = (
props: CartDropdownProps & RouteComponentProps<{}>
) => {
const { cartItems, history } = props;
return (
<div className="cart-dropdown">
<div className="cart-items">
{cartItems.length ? (
cartItems.map(cartItem => (
<CartItem key={cartItem.id} item={cartItem} />
))
) : (
<span className="empty-message">Your cart is empty</span>
)}
</div>
<CustomButton onClick={() => history.push("./checkout")}>
GO TO CHECKOUT
</CustomButton>
</div>
);
};
const mapStateToProps = createStructuredSelector<StoreState, CartDropdownProps>(
{
cartItems: selectCartItems
}
);
export const CartDropdown = withRouter(connect(mapStateToProps)(_CartDropdown));
When we are not passing the 2nd argument to the connect function we can access dispatch function as a prop inside the component right?
Already did with javascript and no complaints but when I'm trying this with typescript dispatch function is not existing in the props.
I console log all the props which this component get and dispatch f exists there.
I don't know why I can't access that!
Can someone help me with this..?

You've provided type of props for _CartDropdown as CartDropdownProps & RouteComponentProps<{}>. This type does not contain dispatch. So from TS point of view dispatch is not present.
console.log logs object as it represented by JS and sees dispatch.
To solve, add type of dispatch to props type like below
import { Dispatch } from 'redux'
const _CartDropdown: React.FC<CartDropdownProps & RouteComponentProps<{}> & {dispatch: Dispatch}> = /* ... */

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.

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

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)
}

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.

I can't access the second level of a json after passing the data for props, in react js

I make a request with axios in a saga, which I receive in a component through the reduction in the mapStateToProps, if I go through the data in that component I can access any level but if I send it on props I can only access the first level of the json and on the second level I get Cannot read property 'route' of undefined
component where I trigger the action and receive the state, and I pass the props:
import React, { Component } from 'react';
import Aux from '../../hoc/Auxiliary';
import Products from '../../Components/ProductsSencillo/Products'
import { Grid } from 'styled-css-grid';
import { connect } from 'react-redux'
import { productComplete } from '../../redux/actions/productAction'
import reducerProduct from '../../redux/modules/reducers/productReducer'
import { bindActionCreators } from 'redux';
class ProductBuilder extends Component {
componentDidMount() {
this.props.handleListar();
}
render() {
return (
<Aux>
<Grid columns="repeat(auto-fit,minmax(45px,1fr))">
<Products products={this.props.state} />
</Grid>
</Aux>
);
}
}
const mapStateToProps = (state) => ({
state: state.productReducer.productos.productos
})
const mapDispatchToProps = dispatch => ({
handleListar: bindActionCreators(productComplete, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(ProductBuilder);
component where I receive the props and the mappings:
import React, { Component } from 'react';
import Product from './Product/Product'
import { Cell } from 'styled-css-grid';
class Products extends Component {
render() {
return {
this.props.products.map(pro => {
return <Cell width={3}>< Product
image={pro.imagen_principal.ruta}
name={pro.nombre}
/>
</Cell>
})
}
}
}
export default Products;
error that throws me:
API:
When accessing "route" you have to first check if the value is present in the object that component receives.
Try this:
<Product image={pro.imagen_principal && pro.imagen_principal.ruta}/>
Hope this helps!

Categories