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

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.

Related

How to dispatch an action from inside getInitialProps?

I am trying to implement Redux in a Next.js app and have problems getting the dispatch function to work in getInitialProps. The store is returned as undefined for some reason that I cannot figure out. I am using next-redux-wrapper. I have followed the documentation on next-redux-wrapper GitHub page but somewhere on the way it goes wrong. I know the code is working - I used axios to directly fetch the artPieces and then it worked just fine but I want to use Redux instead. I am changing an react/express.js app to a Next.js app where I will use the API for the basic server operations needed. This is just a small blog app.
Here is my store.js:
import { createStore } from 'redux';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
// create your reducer
const reducer = (state = { tick: 'init' }, action) => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload };
case 'TICK':
return { ...state, tick: action.payload };
default:
return state;
}
};
// create a makeStore function
const makeStore = (context) => createStore(reducer);
// export an assembled wrapper
export const wrapper = createWrapper(makeStore, { debug: true });
And here is the _app.js:
import './styles/globals.css';
import { wrapper } from '../store';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default wrapper.withRedux(MyApp);
And finally here is where it does not work. Trying to call dispatch on the context to a sub component to _app.js:
import React from 'react';
import { ArtPiecesContainer } from './../components/ArtPiecesContainer';
import { useDispatch } from 'react-redux';
import axios from 'axios';
import { getArtPieces } from '../reducers';
const Art = ({ data, error }) => {
return (
<>
<ArtPiecesContainer artPieces={data} />
</>
);
};
export default Art;
Art.getInitialProps = async ({ ctx }) => {
await ctx.dispatch(getArtPieces());
console.log('DATA FROM GETARTPIECES', data);
return { data: ctx.getState() };
};
This should probably work with "next-redux-wrapper": "^7.0.5"
_app.js
import { wrapper } from '../store'
import React from 'react';
import App from 'next/app';
class MyApp extends App {
static getInitialProps = wrapper.getInitialAppProps(store => async ({Component, ctx}) => {
return {
pageProps: {
// Call page-level getInitialProps
// DON'T FORGET TO PROVIDE STORE TO PAGE
...(Component.getInitialProps ? await Component.getInitialProps({...ctx, store}) : {}),
// Some custom thing for all pages
pathname: ctx.pathname,
},
};
});
render() {
const {Component, pageProps} = this.props;
return (
<Component {...pageProps} />
);
}
}
export default wrapper.withRedux(MyApp);
and Index.js
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { END } from 'redux-saga'
import { wrapper } from '../store'
import { loadData, startClock, tickClock } from '../actions'
import Page from '../components/page'
const Index = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(startClock())
}, [dispatch])
return <Page title="Index Page" linkTo="/other" NavigateTo="Other Page" />
}
Index.getInitialProps = wrapper.getInitialPageProps(store => async (props) => {
store.dispatch(tickClock(false))
if (!store.getState().placeholderData) {
store.dispatch(loadData())
store.dispatch(END)
}
await store.sagaTask.toPromise()
});
export default Index
For the rest of the code you can refer to nextjs/examples/with-redux-saga, but now that I'm posting this answer they're using the older version on next-redux-wrapper ( version 6 ).

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

TypeError: searchField.toLowerCase is not a function when using hooks an redux

I am have been working on a little project to better understand react. I recently converted it to use hooks and I am trying to implement redux, with it. However I get the following error now.
TypeError: searchField.toLowerCase is not a function
looking at the docs, I stopped using connect from react-redux and switched to using useDispatch and useSelector. But I believe I have set up everything correctly but not sure as to why this error being raise.
This is my action.js
import { SEARCH_EVENT } from './searchfield_constants';
export const setSearchField = (payload) => ({ type: SEARCH_EVENT, payload });
This is my reducer
import { SEARCH_EVENT } from './searchfield_constants';
const initialState = {
searchField: '',
};
export const searchRobots = (state = initialState, action = {}) => {
switch (action.type) {
case SEARCH_EVENT:
return { ...state, searchField: action.payload };
default:
return state;
}
};
this is my index.js where I am using the Provider from react-redux
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { searchRobots } from './searchfield/searchfield_reducers';
import './styles/index.css';
import App from './App';
const store = createStore(searchRobots);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
finally here is my App.jsx
import { useState, useEffect, useCallback } from 'react';
import { setSearchField } from './searchfield/searchfield_actions';
import { useDispatch, useSelector } from 'react-redux';
import axios from 'axios';
import React from 'react';
import CardList from './components/CardList';
import SearchBox from './components/SearchBox';
import Scroll from './components/Scroll';
import Error from './components/Error';
import 'tachyons';
import './styles/App.css';
// const mapStateToProps = (state) => ({
// searchField: state.searchField,
// });
// const mapDispatchToProps = (dispatch) => ({
// onSearchChange: (e) => dispatch(setSearchField(e.target.value)),
// });
const App = () => {
const searchField = useSelector(state => state.searchField)
const dispatch = useDispatch();
const [robots, setRobots] = useState([]);
// const [searchField, setSearchField] = useState('');
const fetchUsers = useCallback(async () => {
try {
const result = await axios('//jsonplaceholder.typicode.com/users');
setRobots(result.data);
} catch (error) {
console.log(error);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
fetchUsers();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const filteredRobots = robots.filter((robot) => {
return robot.name.toLowerCase().includes(searchField.toLowerCase());
});
return !robots.length ? (
<h1 className='f1 tc'>Loading...</h1>
) : (
<div className='App tc'>
<h1 className='f1'>RoboFriends</h1>
<SearchBox searchChange={dispatch(setSearchField(e => e.target.value))} />
<Scroll>
<Error>
<CardList robots={filteredRobots} />
</Error>
</Scroll>
</div>
);
};
export default App;
what am I doing wrong?
So the solution was the following,
I created a function called on searchChange, which calls dispatch and then the setSearchField which uses the e.target.value as the payload.
const onSearchChange = (e) => {
dispatch(setSearchField(e.target.value));
};
so the final return looks like the following
return !robots.length ? (
<h1 className='f1 tc'>Loading...</h1>
) : (
<div className='App tc'>
<h1 className='f1'>RoboFriends</h1>
<SearchBox searchChange={onSearchChange} />
<Scroll>
<Error>
<CardList robots={filteredRobots} />
</Error>
</Scroll>
</div>
);
};
In you App.js, convert this line
const searchField = useSelector(state => state.searchField)
to
const { searchField } = useSelector(state => state.searchField)
basically de-structure out searchField from state.searchField
This is attributed to the fact how redux sets state.
In your reducer searchRobots the initial state provided by redux will be
state = {
...state,
searchField
}
and in this line return { ...state, searchField: action.payload };, you're adding
another property searchField to state.searchField object so you'll need to de-structure it out.
It looks like your searchField value is getting set to undefined or some non-string value.
I found this line to be incorrect
<SearchBox searchChange={dispatch(setSearchField(e => e.target.value))} />
It should be changed to
<SearchBox searchChange={() => dispatch(setSearchField(e => e.target.value))} />
So that on search change this function can be called. Currently you are directly calling dispatch and this may be setting your searchField to undefined
Also for safer side before using toLowerCase() convert it to string ie searchField.toString().toLowerCase()

react redux action not calling reducer

I cant for the life of my get one of my actions to call the reducer.
I've written multiple other actions and reducers in this app, which works perfectly.
It is the beginning of a filter function. I have a text input field, where I constantly keep track of the input field state, and dispatch the field value to the redux action. I have a console.log inside the action, which logs every keypress properly.
What I cant seem to understand, is why the reducer isn't called at each keypress. I've tried multiple console.log's inside the reducer, however none of them gets logged with keypresses.
The first console.log inside the reducer is called when I refresh the page.
if I try to log action.type instead, I get:
##redux/PROBE_UNKNOWN_ACTION1.0.i.0.0.9
If I try the same in any of the other reducers I've written in the same app, I get the appropriate type logged out.
Some code:
Filter Component:
import React, { useState } from 'react'
import { useDispatch } from 'react-redux';
import { filterAnecdotes } from '../reducers/filterReducer';
const Filter = () => {
const [value, setValue] = useState("");
const handleChange = (e) => {
setValue(e.target.value)
}
useDispatch(filterAnecdotes(value));
const style = {
marginBottom: 10
}
return (
<div style={style}>
filter <input onChange={handleChange} />
</div>
)
}
export default Filter
Reducer and action:
Here, I haven't figured out how to get the state of all anecdotes, and what to actually return. For now, I'm just trying to have it get called properly.
const filterReducer = (state = null, action) => {
// These logs only get logged on refresh.
// The action.type should be 'SEARCH', but is not.
console.log("From filterReducer")
console.log(action.type)
switch(action.type) {
case 'SEARCH':
// This is not called at all.
console.log(action.type, action.data)
return action.data;
default:
return state
}
}
export const filterAnecdotes = (filter) => {
console.log(filter);
return {
type: 'SEARCH',
data: filter
}
}
export default filterReducer;
Example of a redux file that actually works:
const reducer = (state = [], action) => {
console.log(state, action)
switch(action.type){
case 'NEW_ENTRY_NOTIFICATION':
console.log(action.type)
return [...state, action.data]
case 'NEW_VOTE_NOTIFICATION':
return [...state, action.data]
case 'HIDE_NOTIFICATION':
return []
default:
return state
}
}
export const createNewEntryNotification = (notification) => {
return {
type: 'NEW_ENTRY_NOTIFICATION',
data: notification
}
}
export const createNewVoteNotification = (notification) => {
return {
type: 'NEW_VOTE_NOTIFICATION',
data: notification
}
}
export const hideNotification = () => {
return {
type: 'HIDE_NOTIFICATION'
}
}
export default reducer
App component (should be irrelevant)
import React from 'react';
import NewEntry from './components/AnecdoteForm'
import AnecdoteList from './components/AnecdoteList'
import Notification from './components/Notification'
import Filter from './components/Filter';
const App = () => {
return (
<div>
<h2>Anecdotes</h2>
<Filter />
<Notification />
<AnecdoteList />
<NewEntry />
</div>
)
}
store (should be irrelevant)
import anecdoteReducer from './anecdoteReducer';
import notificationReducer from './notificationReducer';
import filterReducer from './filterReducer';
import { combineReducers } from 'redux'
const reducer = combineReducers({
anecdotes: anecdoteReducer,
notifications: notificationReducer,
filters: filterReducer,
});
export default reducer
index.js (should also be irrelevant)
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './App'
import reducer from './reducers/store'
const store = createStore(reducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
export default App
I'll be happy to provide more information if needed.
This is for a project from fullstackopen.
Try to instantiate useDispatch like this after const [value, setValue] = useState("");
const dispatch = useDispatch();
And then use the intance to dispatch the action
dispatch(filterAnecdotes(value));
the use of useDispatch is misunderstood.
Link for reference: https://react-redux.js.org/api/hooks#usedispatch
You should create a dispatch from useDispatch:
const dispatch = useDispatch();
dispatch(filterAnecdotes(value));

Access dispatch function as a prop in the component

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}> = /* ... */

Categories