im writing a react app who has a default state management: View dispatch an action than change reducer state. I was able to test the view and the reducer but didn't find a way to test my actions file because return a dispatch function
Action File that need to be tested:
import {Dispatch} from 'redux'
import {AuthAction, AuthActionTypes, SetUserAction} from "../actions-types/auth-actions-types";
export const setUserAction = (user: User) => {
return async (dispatch: Dispatch<SetUserAction>) => {
dispatch({
type: AuthActionTypes.SET_USER,
payload: user
})
}
}
reducer
import {AuthAction, AuthActionTypes} from "../actions-types/auth-actions-types";
export const initialAuthState = {
auth: {},
user: null
};
const reducer = (state = initialAuthState, action: AuthAction) => {
switch(action.type) {
case AuthActionTypes.SET_USER:
return {
...state,
user: action.payload,
};
default:
return state
}
}
export default reducer
reducer Test working ok.
import authReducer, {initialAuthState} from "./auth-reducer";
import {AuthActionTypes} from "../actions-types/auth-actions-types";
describe('Auth Reducer', ()=>{
test('should return user correclty ', ()=>{
const mockPayload = {
name: 'any_name',
emaiL: 'any_email',
accessToken: 'any_tokem'
}
const newState = authReducer(initialAuthState, {
type: AuthActionTypes.SET_USER,
payload: mockPayload
})
expect(newState.user).toEqual(mockPayload);
})
})
Action File test with problems
describe('AuthAction', ()=>{
test('setUserAction', ()=>{
const user = {
name: 'any_user',
email: 'any_email',
token: 'any_token'
}
const result = setUserAction();
expect(result).toEqual(user);
})
})
Expected: {"email": "any_email", "name": "any_user", "token": "any_token"}
Received: [Function anonymous]
Writing an action creator
Here is the official documentation that shows how to create an action creator
I do not see the benefit for your action creator to do a dispatch, you can simply write it and use it in the following way:
// action.ts
import { Dispatch } from 'redux'
import { AuthAction, AuthActionTypes, SetUserAction } from "../actions-types/auth-actions-types";
export const setUser = (user: User) => ({
type: AuthActionTypes.SET_USER,
payload: user
})
// somewhere.ts
dispatch(setUser(user))
Now the redux team recommends using redux-toolkit and they provide a simple tool called createAction
And if you want to create your reducer and action creator at the same time in the easier possible way you can use createSlice
How to test a reducer and an action?
To avoid an opinionated response to this answer you have two paths:
testing reducer with your action creator
a test for the reducer and a test for the action
Testing a reducer with your action creator
The reducer test should confirm that the triggered action has the expected impact.
Here is an example of using your reducer and your action creator together:
describe('Auth Reducer', ()=>{
test('should set user correctly', ()=> {
const newState = authReducer(initialAuthState, setUser(mockPayload))
expect(newState.user).toEqual(mockPayload);
})
})
The benefit of this is that you just write one test and you assert that both action creator and reducer work well together.
How to test an action creator alone?
You do not need to test your action creator if you test your reducer with it.
An action is just an object with a type and payload basically, so you can test it in the following way
describe('AuthAction', () => {
test('setUserAction', () => {
const user = {
name: 'any_user',
email: 'any_email',
token: 'any_token'
}
const result = setUser(user);
expect(result).toEqual({ type: AuthActionTypes.SET_USER, user });
})
})
Related
So I have a movie app, and I have a page for a single movie. I have a section on that page where I display all of the videos from an API related to a certain movie.
So my Videos component looks like this:
const Videos = ({videos} :{videos:IVideos | null}) => {
return (
<div>{videos?.results.map((video, i) =>
<div key={i}>{video.name}</div>
)}</div>
)
}
It's just a basic component which gets props from a higher component. But the main thing is redux slice, which looks like this:
Initial state:
const initialState: IMovieVideosState = {
movieVideos: null,
fetchStatus: null,
}
export interface IMovieVideosState {
movieVideos: IVideos | null;
fetchStatus: FetchStatus | null;
}
And finally slice:
const videosSlice = createSlice({
name:'videos',
initialState,
reducers:{},
extraReducers(builder) {
builder
.addCase(fetchVideos.pending, (state, action) => {
state.fetchStatus = FetchStatus.PENDING
})
.addCase(fetchVideos.fulfilled, (state, action) => {
state.fetchStatus = FetchStatus.SUCCESS
state.movieVideos = action.payload
})
.addCase(fetchVideos.rejected, (state, action) => {
state.fetchStatus = FetchStatus.FAILURE
//state.error = action.error.message
})
}
})
As you see, these are basic reducers, where if promise is successful I assign payload to an existing array.
And also thunk function:
export const fetchVideos = createAsyncThunk('videos/fetchVideos', async (id: number) => {
const response = await axios.get<IVideos>(`${API_BASE}movie/${id}/videos?api_key=${TMDB_API_KEY}`);
console.log(response.data);
return response.data;
})
But in the browser I have the next error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
And also another one:
A non-serializable value was detected in an action, in the path: `<root>`. Value:
Promise { <state>: "pending" }
Take a look at the logic that dispatched this action:
Promise { <state>: "pending" }
I have no idea why I could have these errors, because my reducer is the same as another one in my project, but this one doesn't work for some reason.
UseEffect for dispatching all reducers:
useEffect(() =>{
dispatch(fetchDetail(Number(id)));
dispatch(fetchCredits(Number(id)));
dispatch(fetchPhotos(Number(id)));
dispatch(fetchRecommended(Number(id)));
dispatch(fetchSimilar(Number(id)));
dispatch(fetchVideos(Number(id))); //dispatching fetchVideos()
}, [dispatch, id])
So in my case, all of the other functions work fine besides fetchVideos().
Another example of a thunk for movie details:
export const fetchDetail = createAsyncThunk('detail/fetchDetail', async (id: number) => {
const response = await axios.get<IMovie>(`${API_BASE}movie/${id}?api_key=${TMDB_API_KEY}`);
console.log(response.data);
return response.data;
})
My store file:
import thunk from "redux-thunk";
export const store = configureStore({
reducer: {
popular,
top_rated,
playing,
upcoming,
detail,
credits,
videos,
photos,
recommended,
similar
},
middleware: [thunk]
})
export type RootState = ReturnType<typeof store.getState>;
instead of using create Async Thunk method add think malware where you create store of videos then you can pass Async actions into it without nothing.
import { applyMiddleware, combineReducers, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
// import your videos reducer here from file
export interface State {
videos: IVideos;
}
const rootReducer = combineReducers<State>({
videos: VideosReducer,
});
export const rootStore = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
I'm new in react-redux, and I have a problem with communication between reducer and store.
This is the idea on which I base:
I have a component "Login", that contains a button and two text inputs, and when i click that, I send the action to the reducer. Then, I update the state, and send it to the UI again (thats the way i understand the logic, correct me if i'm wrong). The problem occurs in the reducer, it never enter there, but yes in the action file (tested with console.logs). Maybe the connect is not working? or is in the store part?
Here I detach how I did it
action.js, with two operations only
const logout = () => {
return {
type: "USER_LOGOUT",
payload: false,
};
};
const login = () => {
return {
type: "USER_LOGIN",
payload: true,
};
};
export { logout, login };
reducer.js implementation, only change one boolean value
const initialState = {
logged: false,
};
export default (state = initialState, action) => {
if (action.type === "USER_LOGOUT") {
return {
...state,
logged: false,
};
}
if (action.type === "USER_LOGIN") {
return {
...state,
logged: true,
};
}
return state;
};
index.js (store), here's how i declare the store part
import { createStore, combineReducers } from "redux";
import loginReducer from "./reducer";
const reducers = combineReducers({
loginReducer,
});
const store = createStore(reducers);
export default store;
Login.js, only the touchable part
import { logout, login } from "../redux/actions";
import { connect } from "react-redux";
...
connect(null, { logout, login }, Login);
...
<TouchableOpacity>
...
onPress={() => checkValidation()}
...
</TouchableOpacity>
Here checkValidation, who calls the action "login"
checkValidation() =>
...
login();
...
You are not dispatching the action. To make Redux aware of an action you must dispatch it.
If you are using a class component you need to connect the component and pass it the dispatch action from redux.
I suggest you to use the hooks because its way easier.
1-Import the useDispatch hook
import { useDispatch } from "react-redux";
2-Create the dispatch:
const dispatch = useDispatch();
3-Dispatch your action:
checkValidation() =>
...
// Since your function login already returns the action object:
dispatch(login());
...
I made a todo list a while ago as a way to practice react and redux. Now I'm trying to rewrite it with redux toolkit and having some trouble with the action creators.
Here is the old actions creator:
export const changeDescription = (event) => ({
type: 'DESCRIPTION_CHANGED',
payload: event.target.value })
export const search = () => {
return (dispatch, getState) => {
const description = getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
axios.get(`${URL}?sort=-createdAt${search}`)
.then(resp => dispatch({ type: 'TODO_SEARCHED', payload: resp.data }))
} }
export const add = (description) => {
return dispatch => {
axios.post(URL, { description })
.then(() => dispatch(clear()))
.then(() => dispatch(search()))
} }
export const markAsDone = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: true })
.then(() => dispatch(search()))
} }
export const markAsPending = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: false })
.then(() => dispatch(search()))
} }
export const remove = (todo) => {
return dispatch => {
axios.delete(`${URL}/${todo._id}`)
.then(() => dispatch(search()))
} }
export const clear = () => {
return [{ type: 'TODO_CLEAR' }, search()] }
Now this is the one that I'm working on, I'm trying to replicate the actions of the old one but using redux toolkit:
export const fetchTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
const response = await axios.get(`${URL}?sort=-createdAt${search}`)
return response.data
})
export const addTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const response = await axios.post(URL, {description})
return response.data
})
export const todoReducer = createSlice({
name: 'counter',
initialState: {
description: '',
list: []
},
reducers: {
descriptionChanged(state, action) {
return {...state, dedescription: action.payload}
},
descriptionCleared(state, action) {
return {...state, dedescription: ''}
},
},
extraReducers: builder => {
builder
.addCase(fetchTodos.fulfilled, (state, action) => {
const todo = action.payload
return {...state, list: action.payload}
})
.addCase(addTodos.fulfilled, (state, action) => {
let newList = state.list
newList.push(action.payload)
return {...state, list: newList}
})
}
})
The thing is, I can't find anywhere how to export my extra reducers so I can use them. Haven't found anything in the docs. Can someone help?
extraReducers
Calling createSlice creates a slice object with properties reducers and actions based on your arguments. The difference between reducers and extraReducers is that only the reducers property generates matching action creators. But both will add the necessary functionality to the reducer.
You have correctly included your thunk reducers in the extraReducers property because you don't need to generate action creators for these, since you'll use your thunk action creator.
You can just export todoReducer.reducer (personaly I would call it todoSlice). The reducer function that is created includes both the reducers and the extra reducers.
Edit: Actions vs. Reducers
It seems that you are confused by some of the terminology here. The slice object created by createSlice (your todoReducer variable) is an object which contains both a reducer and actions.
The reducer is a single function which takes the previous state and an action and returns the next state. The only place in your app when you use the reducer is to create the store (by calling createStore or configureStore).
An action in redux are the things that you dispatch. You will use these in your components. In your code there are four action creator functions: two which you created with createAsyncThunk and two which were created by createSlice. Those two will be in the actions object todoReducer.actions.
Exporting Individually
You can export each of your action creators individually and import them like:
import {fetchTodos, descriptionChanged} from "./path/file";
Your fetchTodos and addTodos are already exported. The other two you can destructure and export like this:
export const {descriptionChanged, descriptionCleared} = todoReducer.actions;
You would call them in your components like:
dispatch(fetchTodos())
Exporting Together
You might instead choose to export a single object with all of your actions. In order to do that you would combine your thunks with the slice action creators.
export const todoActions = {
...todoReducer.actions,
fetchTodos,
addTodos
}
You would import like this:
import {todoActions} from "./path/file";
And call like this:
dispatch(todoActions.fetchTodos())
My project consists of a backend(nodejs , express, mysql) and a frontend (reactjs, redux).
The flow of a rendered component is in a simple redux pattern-
in ComponentDidMount I call an action creator this.props.getResource()
in action creator I use axios to call the backend and dispatch an action in callback like so :
actions.js
export const getResource = () => dispatch => {
axios.get(API_URL/path/to/resource)
.then(res => {
dispatch({
type: SOME_RESOURCE,
payload: res.data
});
})
.catch(e =>
dispatch({
type: ERROR,
payload: e
})
);
};
in reducer I send back to component the state with the new array :
reducers.js
export default function(state = initialState, action) {
switch (action.type) {
case SOME_RESOURCE:
return {
...state,
resources: [...state.resources, action.payload] // add new resource to existing array
};
}
default: return state;
}
}
It is working as it should using REST APIs but now I wish to replace a certain API call with a socket so that data is shown in real-time without needing to refresh the page.
How can I convert above example to use sockets instead of API calls?
This is what I have tried:
Flow starts the same - I call an action creator in ComponentDidMount
I changed the action creator to the following :
actions.js
import io from 'socket.io-client';
const socket = io(); // localhost backend
export const getResource= () => dispatch => {
socket
.on("getResourceEvent", res => {
dispatch({
type: SOME_RESOURCE,
payload: res.data
});
})
.on("onError", e => {
dispatch({
type: ERROR,
payload: e
});
});
};
no changes in reducers.js
This works but with each rendering of the component, the store.getState() gets called 1 additional time. On first render getState() is called 1 time and if I refresh the page I get 2 calls from getState() and so on.
What's causing this behavior and how can I prevent it?
Edit:
store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const initialState = {};
const middleware = [ thunk ];
var createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
var store = createStoreWithMiddleware(
rootReducer,
initialState,
applyMiddleware(...middleware)
)
store.subscribe(() => console.log("Store.getState()", store.getState()))
export default store;
Basically what I wanted to do was to stop making axios calls inside of my component. So I thought; “Why not just create an action for that?”
I googled around to find a good “guide” to use Redux and this is what I’m using:
Add a constant to the constants file. Something like const GREAT_COURSE = GREAT_COURSE
Add an action creator to the actions folder. Return an action JavaScript object with a type of the constant you created.
Add a reducer to the reducers folder that handles this action creator.
So I began to create my action creator:
import axios from 'axios'
import { CUSTOMER_FETCH } from './types'
import settings from '../settings'
axios.defaults.baseURL = settings.hostname
export const customers = () => {
return dispatch => {
return axios.get('http://hejhej/customers').then(res => {
dispatch({
type: CUSTOMER_FETCH,
data: res.data
})
})
}
}
And later to add a reducer that handles my action creator:
import { CUSTOMER_FETCH } from '../actions/types'
const initial = []
const customer = action => {
return {
data: action.data
}
}
const customers = (state = initial, action) => {
switch (action.type) {
case CUSTOMER_FETCH:
customers = [...state, customer(action)]
console.log('customers as state', customers)
return customers
default:
return state
}
}
export default customers
And inside of my component I'm importing it:
import { customers } from '../../actions/customersAction'
And later using connect: export default connect(null, { customers })(Events)
And finally I'm using it inside of my component:
customers() {
this.props.customers(this.state.data)
}
So I'm wondering what I'm doing wrong, because I can't see my console.log in my dev tools. Thanks a lot for reading!
Inside of my component atm:
axios.get('http://hejhej/customers').then(res => {
this.setState({
res,
customer: res.data
})
})