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;
Related
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 });
})
})
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 am trying to understand how React, Redux and Axios work together but I just hit a wall and I need some help ...
My problem is that inside the action there is a dispatch but after i return the dispatch it does not continue further.
It's most likely that I do not understand how this works so please try to explain in as much as possible details. Thanks in advance.
my combineReducer
import {combineReducers} from "redux";
import getAvailableDatesReducer from "./getAvailableDatesReducer";
export default combineReducers({
availableDates: getAvailableDatesReducer
});
my reducer
import {FETCH_AVAILABLE_DATES} from "../actions/types";
const initialState = {
availableDates: null
};
const getAvailableDatesReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_AVAILABLE_DATES:
return {...state, availableDates: action.availableDates};
default:
console.log('just default..');
return state;
}
}
export default getAvailableDatesReducer;
my action
export const fetchAvailableDates = (appointmentKey) => {
//return (dispatch) => {
axios.post('/app_dev.php/termin/getavailability/new', {
appointmentKey: appointmentKey
}).then((response) => {
console.log('response received...');
return (dispatch) => {
console.log('not hitting this...');
dispatch({type: FETCH_AVAILABLE_DATES, availableDates: response.data.availability});
};
}).catch(err => {
console.log(err);
});
//}
}
my component
import {fetchAvailableDates} from "../actions";
const Calendar = (props, appointmentKey) => {
useEffect(() => {
fetchAvailableDates(appointmentKey);
}, []);
const mapStateToProps = (state) => {
return {
availableDates: state.availableDates,
}
}
export default connect(mapStateToProps, {fetchAvailableDates})(Calendar);
my index.js file
import {Provider} from 'react-redux';
import thunk from 'redux-thunk';
import {applyMiddleware, compose, createStore} from "redux";
import reducers from './reducers';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root')
);
First, create an action in your action file:
const fetchDatesAction = (response) => ({
type: FETCH_AVAILABLE_DATES,
availableDates: response.data.availability,
});
Then, update connect
export default connect(mapStateToProps, {fetchDatesAction})(Calendar);
Finally, Call api in useEffect, like this:
useEffect(() => {
axios
.post("/app_dev.php/termin/getavailability/new", {
appointmentKey: appointmentKey,
})
.then((response) => {
console.log("response received...");
return (dispatch) => {
console.log("not hitting this...");
props.fetchDatesAction();
};
})
.catch((err) => {
console.log(err);
});
}, []);
Thunk is a library which is responsible for handling side-effects in state management for redux.
Redux is a simple pure function which accepts state and action as an input and based on these two, it returns a new state. So its pretty simple and straight forward.
Now in certain scenarios like the one you have mentioned in your example, we need to perform some asynchronous actions which may not provide immediate result but a promise. In that case, we need to use a third party tool which is also called as enhancer.
That's the reason why you have added
const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
Now when asynchronous action is triggered, it goes to thunk. Thunk processes the request and then triggers one more action which again goes to reducer.
Now reducer being a pure function, does not distinguish between these sources of event and simply update the state based on action and its payload.
Hope this diagram helps you understand the concept.
https://miro.medium.com/max/1400/1*QERgzuzphdQz4e0fNs1CFQ.gif
So I'm using TS React, Redux, and Thunk middleware to handle redux actions that communicate with my api but I cant seem to get the initial configuration for my action function.
My action function is as follows:
export const startSession = ((accessCode: string) => {
return async (dispatch: Dispatch): Promise<Action> => {
try {
const response = await apiCall(accessCode);
return dispatch({ type: SessionActions.START_SESSION, payload: response });
} catch (e) {
console.log('error', e)
}
};
});
I have also tried this:
export const startSession = ((accessCode: string) => {
return async (dispatch: Dispatch) => {
try {
await apiCall(accessCode)
.then(response => dispatch({ type: SessionActions.START_SESSION, payload: response }))
} catch (e) {
console.log('error', e)
}
};
})
but neither seems to work. I thought waiting for the api response would force redux to wait, but it seems to be returning the promise into the state - shown in my redux-logger:
action undefined # 19:10:17.807
redux-logger.js?d665:1 prev state: {some state}
redux-logger.js?d665:1 action: PromiseĀ {<pending>}
And I get the error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
I noticed that this dispatched type is undefined, so there must me a dispatch call being made initially before the data is returned from the api. If anyone could explain to me why it does this, and the standard format for writing actions that use thunk that would be super helpful.
Also please let me know if there is information that I'm missing.
Someone below asked to see how I the initialized store with thunk:
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger';
import { createBrowserHistory } from 'history';
import rootReducer from '../_reducers/index'
const loggerMiddleware = createLogger();
export const history = createBrowserHistory();
export const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
);
I used this guide to set up authentication with my api using jwt token authentication from Django rest framework. I can log in just fine. But currently I set up another endpoint to get the user info of the currently authorized user (determined by the token they send with the request).
It's dispatched like so (done some time after the user logs in)
fetchUser: () => dispatch(loadUser()),
My action to load username:
import { RSAA } from 'redux-api-middleware';
import withAuth from '../reducers'
export const USER_REQUEST = '##user/USER_REQUEST';
export const USER_SUCCESS = '##user/USER_SUCCESS';
export const USER_FAILURE = '##user/USER_FAILURE';
export const loadUser = () => ({
[RSAA]: {
endpoint: '/api/user/info/',
method: 'GET',
headers: withAuth({}),
types: [
USER_REQUEST, USER_SUCCESS, USER_FAILURE
]
}
});
Reducer:
import jwtDecode from 'jwt-decode'
import * as user from '../actions/user'
const initialState = {};
export default (state=initialState, action) => {
switch(action.type) {
case user.USER_SUCCESS:
return action.payload;
default:
return state
}
}
export const userInfo = (state) => state.user;
Reducer index.js:
export function withAuth(headers={}) {
return (state) => ({
...headers,
'Authorization': `Bearer ${accessToken(state)}`
})
}
export const userInfo = state => fromUser.userInfo(state.user);
But when I try to get the user info of the logged in user, I get an error..
TypeError: Cannot read property 'type' of undefined
Why is the action type undefined?
Note: I tagged this with thunk because my project does use thunk, but not for this bit of redux.
Edit: My middleware.js and my store.js.
The issue is that in store.js that thunk is applied before the redux-api-middleware middleware.
Just move it to after like this:
const store = createStore(
reducer, {},
compose(
applyMiddleware(apiMiddleware, thunk, routerMiddleware(history)),
window.devToolsExtension ? window.devToolsExtension() : f => f,
),
);