Testing buttons in react dynamically rendered with data from redux - reactjs

This is a simplified example I've made.
I have the following react component
Test.tsx
import * as React from 'react';
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../app/hooks';
import { selectTest, setTest } from './testslice';
const Test: React.FunctionComponent = (props) => {
const vals = useAppSelector(selectTest)
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(setTest(["2","3","4","5"]))
},[])
return <>
{vals.map((v,i) => <button key={i}>{v}</button>)}
</>;
};
export default Test;
And the following redux reducer slice
testSlice.ts
import { createSlice, PayloadAction } from "#reduxjs/toolkit";
import { RootState } from "../app/store";
export interface AppState {
test:string[]
}
const initialState: AppState = {
test:[]
};
export const appSlice = createSlice({
name: 'test',
initialState,
reducers: {
setTest(state,action: PayloadAction<string[]>) {
state.test = action.payload
}
},
});
export const {
setTest,
} = appSlice.actions;
export const selectTest = (state: RootState) => state.test.test;
export default appSlice.reducer;
I would like to test the Test component and see, that the buttons are rendered with the values I've dispatched to the redux store (The length of values will be a fixed length)
Test.test.tsx
import React from 'react';
import { render } from '#testing-library/react';
import { Provider } from 'react-redux';
import { store } from '../app/store';
import Test from './TestComponent';
test('renders learn react link', () => {
const { getByText } = render(
<Provider store={store}>
<Test/>
</Provider>
);
//Somehow test that the buttons rendered in <Test/> component have the values dispatched in the useEffect hook
});
How can I achieve this?

Please have a look at the official documentation of testing redux with react and testing library. The idea is to create a preloadedState which gets injected in your application from inside the test. You can then test for the objects of this preloadedState and see if the objects are rendered correctly. After setting up the helper render function in the docs above with e.g.
...
import appReducer from 'PATH/testSlice';
...
store = configureStore({ reducer: { test: appReducer }, preloadedState })
you can then do:
...
import { render } from '../../test-utils'
...
const givenState = { test: ["1", "2", "3"] }
const { getByText } = render( <Test/>, { preloadedState: givenState });
for(const val of givenState.test) {
expect(getByText(val).toBeVisible();
}
For more "integration"-testing, you could also mock return values from functions that finally populate the state, e.g. fetch calls. That way, you would not need to create a preloadedState but mock the fetch calls, whose objects you would use for assertions.

Related

how to use next-redux-wrapper on next JS 13?

i'm using the app dir in next js 13 and redux. i have multiple redux reducers so i needed to combine them using next-redux-wrapper package. when using the old way with export default wrapper.withRedux(RootLayout); it works but not for other browsers. and it shows a warning /!\ You are using legacy implementaion. Please update your code: use createWrapper() and wrapper.useWrappedStore().. so i updated the code in my layout.js.
"use client";
import { ChakraProvider } from "#chakra-ui/react";
import "../styles/globals.css";
import { Provider } from "react-redux";
import { wrapper } from "../store";
function RootLayout({ children }) {
const { store } = wrapper.useWrappedStore();
return (
<html>
<head />
<body className="max-w-7xl mx-auto">
<Provider store={store}>
<ChakraProvider>{children}</ChakraProvider>
</Provider>
</body>
</html>
);
}
export default RootLayout;
but when i use this it show there is an error on const { store } = wrapper.useWrappedStore(); line TypeError: Cannot read properties of undefined (reading 'initialState'). so how can i use redux with Next JS 13?
store.js
import { combineReducers, configureStore } from "#reduxjs/toolkit";
import { createWrapper, HYDRATE } from "next-redux-wrapper";
import menuReducer from "./slices/menuSlice";
import livingReducer from "./slices/livingSlice";
import {} from "react-redux";
import { ActionTypes } from "#mui/base";
const combinedReducer = combineReducers({
menu: menuReducer,
living: livingReducer,
});
const reducer = (state, action) => {
if (ActionTypes.type === HYDRATE) {
const nextState = {
...state,
...action.payload,
};
return nextState;
} else {
return combinedReducer(state, action);
}
};
const store = configureStore({
reducer: reducer,
});
const makeStore = () => store;
export const wrapper = createWrapper(makeStore);
please any help
From looking at the source code for useWrappedStore, it is expecting the initial state to be passed into the function call. Something like this:
const { store } = wrapper.useWrappedStore({ initialState: { foo: "bar" } });
This should fix your error.

get initialState value in reducer from action inRedux

I am new in redux, I created a initialState in reducer.jsx like that:
const initState = {
loading: false,
todos: [],
todo: null,
};
Then in action.jsx, I want to use the value 'todo' of initState. How can I do that. I try to use useSelector but it is impossible because useSelector cannot be used in action
Try to learn redux-toolkit. It's a lot easier to learn than redux and createSlice api in redux-toolkit is what you find right now.
https://redux-toolkit.js.org/api/createslice
First of Create store using redux then install react-redux library for state management with react. follow code below step by step.
Step - 1
// redux.js
import { createStore } from 'redux';
const store = createStore(rootReducer);
export default store;
Step - 2
// rootReducer.js
import { combineReducers } from 'redux';
import todosReducer from './todosReducer';
const rootReducer = combineReducers({
todos: todosReducer,
});
export default rootReducer;
Step - 3
// todosReducer.js
const initState = {
loading: false,
todos: [],
todo: null,
};
const todosReducer = (state = initState, action) {
switch(action.type) {
case 'add_todo': {
return [
...state,
action.payload.todo
]
}
default:
return state;
}
}
Step - 4
//todosAction.js
export const addTodo = (todo) => {
return({
type:'add_todo',
payload: {
todo
}
})
};
Step - 5
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from '../store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
you can avoid file path choose yourself. import correct file path.
Step - 5
// Todos.js
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {addTodo } from './todosActions';
const Todos = () => {
const { todos } = useSelector(state => state.todos);
const dispatch = useDispatch();
useEffect(() => {
console.log("todos", todos); // show all todos state
} ,[])
const todo = {
id: 1,
title: "redux is state management library"
}
const todoHandler = () => {
dispatch( addTodo(todo));
}
return(
<div>
// Todos Here
<button onClick={todoHandler}> add todo </button>
</div>
);
}

How to set InitialState in React Testing Library

I am writing a test where I need to render a component, but the rendering of my component is not working and I am receiving this error:
Uncaught [TypeError: Cannot read property 'role' of undefined].
This is because in the componentDidMount function in my component I am checking if this.props.authentication.user.role === 'EXPERT'. However, this.props.authentication has user as undefined.
This is the correct initialState for my program, but for the test I want to set my initialState to have a user object. That is why I redefine initialState in my test. However, the component does not render with that new initialState.
Here is the testing file:
import { Component } from '../Component.js';
import React from 'react';
import { MemoryRouter, Router } from 'react-router-dom';
import { render, cleanup, waitFor } from '../../test-utils.js';
import '#testing-library/jest-dom/extend-expect';
afterEach(cleanup)
describe('Component Testing', () => {
test('Loading text appears', async () => {
const { getByTestId } = render(
<MemoryRouter><Component /></MemoryRouter>,
{
initialState: {
authentication: {
user: { role: "MEMBER", memberID:'1234' }
}
}
},
);
let label = getByTestId('loading-text')
expect(label).toBeTruthy()
})
});
Here is the Component file:
class Component extends React.Component {
constructor(props) {
super(props)
this.state = {
tasks: [],
loading: true,
}
this.loadTasks = this.loadTasks.bind(this)
}
componentDidMount() {
if (
this.props.authentication.user.role == 'EXPERT' ||
this.props.authentication.user.role == 'ADMIN'
) {
this.loadTasks(this.props.location.state.member)
} else {
this.loadTasks(this.props.authentication.user.memberID)
}
}
mapState(state) {
const { tasks } = state.tasks
return {
tasks: state.tasks,
authentication: state.authentication
}
}
}
I am also using a custom render function that is below
import React from 'react'
import { render as rtlRender } from '#testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { initialState as reducerInitialState, reducer } from './_reducers'
import rootReducer from './_reducers'
import configureStore from './ConfigureStore.js';
import { createMemoryHistory } from 'history'
function render(ui, {
initialState = reducerInitialState,
store = configureStore({}),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
// override render method
export { render }
Perhaps I am coming super late to the party but maybe this can serve to someone. What I have done for a Typescript setup is the following (all this is within test-utils.tsx)
const AllProviders = ({
children,
initialState,
}: {
children: React.ReactNode
initialState?: RootState
}) => {
return (
<ThemeProvider>
<Provider store={generateStoreWithInitialState(initialState || {})}>
<FlagsProvider value={flags}>
<Router>
<Route
render={({ location }) => {
return (
<HeaderContextProvider>
{React.cloneElement(children as React.ReactElement, {
location,
})}
</HeaderContextProvider>
)
}}
/>
</Router>
</FlagsProvider>
</Provider>
</ThemeProvider>
)
}
interface CustomRenderProps extends RenderOptions {
initialState?: RootState
}
const customRender = (
ui: React.ReactElement,
customRenderProps: CustomRenderProps = {}
) => {
const { initialState, ...renderProps } = customRenderProps
return render(ui, {
wrapper: (props) => (
<AllProviders initialState={initialState}>{props.children}</AllProviders>
),
...renderProps,
})
}
export * from '#testing-library/react'
export { customRender as render }
Worth to mention that you can/should remove the providers that doesn't make any sense for your case (like probably the FlagsProvider or the HeaderContextProvider) but I leave to illustrate I decided to keep UI providers within the route and the others outside (but this is me making not much sense anyway)
In terms of the store file I did this:
//...omitting extra stuff
const storeConfig = {
// All your store setup some TS infer types may be a extra challenge to solve
}
export const store = configureStore(storeConfig)
export const generateStoreWithInitialState = (initialState: Partial<RootState>) =>
configureStore({ ...storeConfig, preloadedState: initialState })
//...omitting extra stuff
Cheers! 🍻
I am not sure what you are doing in the configure store, but I suppose the initial state of your component should be passed in the store.
import React from 'react'
import { render as rtlRender } from '#testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { initialState as reducerInitialState, reducer } from './_reducers'
import { createMemoryHistory } from 'history'
function render(
ui,
{
initialState = reducerInitialState,
store = createStore(reducer,initialState),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
// override render method
export { render }
I hope it will help you :)

Error: Actions must be plain objects. Use custom middleware for async actions.- Even the actions contain the method

i am new to react , just understanding the concept on redux without using redux thunk. please see the below code
// app.js
import React, { Component } from 'react';
import {connect} from 'react-redux';
import * as actions from './actions'
class App extends Component {
render() {
return (
<div>
<button onClick={this.props.fetchData}>Show Data</button>
</div>
);
}
}
const mapStateToProps = state => {
return {
}
}
const mapDispatchToProps = dispatch => {
return {
fetchData: () => dispatch(actions.fetchDataHandler)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import {createStore} from 'redux';
import Data from './reducers';
import {Provider} from 'react-redux';
const store = createStore(Data)
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
registerServiceWorker();
//actions-index.js
export const fetchDataHandler = e => dispatch => {
console.log("hit here");
}
//reducers-index.js
// default state
const defaultState = {
data: null
}
let data;
export default data = (state=defaultState, action) => {
switch(action.type){
case "FETCH_DATA":
return {
...state,
data: action.payload
}
default:
return{
...state
}
}
}
folder structure is
src
actions
index.js
reducers
index.js
app.js
i am not using redux thunk, when the button is clicked it will call the fetchData which will invoke the actions.fetchDataHandler
so on the console it should get a message as "hit here", but its not working.
sorry if i am not understanding the redux concept properly.
In a normal redux flow, Actions are supposed to be plain object, i.e an action creator must return a plain object, But in your case since you haven't need using any middleware like redux-thunk, you can not write an action like
//actions-index.js
export const fetchDataHandler = e => dispatch => {
console.log("hit here");
}
A general way to do it would be
export const fetchDataHandler = e => {
console.log("hit here");
return {
type: 'MY_ACTION'
}
}
However if you configure a middleware like redux-thunk, you can have an asynchronous action within your action creators like
//actions-index.js
export const fetchDataHandler = e => dispatch => {
console.log("hit here");
API.call().then((res) => {
dispatch({ type: 'API_SUCCESS', payload: res });
});
}
Also your mapDispatchToProps isn't calling the action in dispatch, you would either write it like
const mapDispatchToProps = dispatch => {
return {
fetchData: () => dispatch(actions.fetchDataHandler())
}
}
or
const mapDispatchToProps = {
fetchData: actions.fetchDataHandler
}

not getting synchronous data from redux thunk

action creator
export function pickup(latlng) {
return function(dispatch) {
dispatch({ type: PICKUP_STATE,payload:latlng });
};
}
Reducer
import {
PICKUP_STATE,
PICKUP_ADD,
DROPOFF_STATE
} from '../actions/types';
export default (state={},action) => {
const INITIAL_STATE = {
pickup: '',
pickupAdd:''
};
switch(action.type) {
case PICKUP_STATE:
console.log(action.payload)
return {...state,pickup:action.payload};
case PICKUP_ADD:
return{...state,pickupAdd:action.payload};
case DROPOFF_STATE:
return {...state,dropoff:action.payload}
default:
return state;
}
//return state;
}
Map component
import {
connect
} from "react-redux";
import * as actions from "../actions"
class Map extends React.Component {
componentWillReceiveProps(nextprops) {
if (nextprops.pickupProps !== undefined) {
this.setState({
pick: nextprops.pickupProps
}, () => {
console.log(this.state.pick);
});
}
}
isPickEmpty(emptyPickState) {
this.props.pickup(emptyPickState);
// setTimeout(() =>{ console.log('sdkjlfjlksd',this.state.pick)
},3000);
console.log(this.state.pick);
}
}
const mapStateToProps = (state) => {
// console.log(state.BookingData.pickup);
return {
pickupProps:state.BookingData.pickup,
pickupAddProps: state.BookingData.pickupAdd
}
}
export default connect(mapStateToProps,actions)(Map);
app.js root file
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import "normalize.css/normalize.css"
import "./styles/styles.scss";
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import reduxThunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import AppRouter from './routers/AppRouter';
import reducers from './reducers';
import {AUTH_USER} from "./actions/types";
const middleware = [
reduxThunk,
];
const store = createStore(reducers, composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
));
const token = localStorage.getItem('token');
if(token){
store.dispatch({type:AUTH_USER});
}
ReactDOM.render(
<Provider store={store}>
<AppRouter />
</Provider>
, document.getElementById('app'));
here my problem is when i'm calling isPickEmpty() from my map component
it invoke action creator this.props.pickup(false) (i also checked in redux-devtools it show false value) then i'm consoling pick state( which store in componentWillReceiveProps(nextprops)) so it showing default value instead of false but when i'm consoling the value inside setTimeout(() =>{console.log('sdkjlfjlksd',this.state.pick) }, 3000); it showing false value
correct me if i'm wrong what i know that redux-thunks works in synchronous manner not asynchronous manner so here why it's not working in synchronous manner
i'm stuck,plz anyone help me!
Update
i just got where the prblm, actually in componentWillReceiveProps where i'm setting pick state value because it is asynchronous so when i'm fetching the value in isPickEmpty function i'm getting prev value.
how handle setState or is there any way to solve
At the component you use the values in BookingData, but on the reducer you add it direct to the state.
const mapStateToProps = (state) => {
console.log(state);//Check the state here
return {
pickupProps:state.pickup,
pickupAddProps: state.pickupAdd
}
}
Should work well if you se this mapStateToProps

Resources