I am trying to use socket.io and redux-socket.io to simply keep data stores in sync for mutliple clients.
Each client will FETCH their own initial list of "todos" which will be put into their redux store.
Then when a client performs and ADD, UPDATE, or DELETE, I want it broadcast to the other clients to update their own redux store; here's what I have -
client setup
let socketIoMiddleware = createSocketIoMiddleware(socket, (type, action) => {
// emit to server if one of these actions
return (type === 'ADD_TODO_SUCCESS' ||
type === 'UPDATE_TODO_SUCCESS' ||
type === 'DELETE_TODO_SUCCESS')
});
server
socket.on('action', (action) => {
socket.broadcast.emit('action', action);
}
}
My goal was to not have to change my redux code and just plug socket.io in, but what I think is happening is, for ex:
Client A: dispatch({type: 'UPDATE_TODO_SUCESS', payload: todo });
middleware: emit('action', {type: 'UPDATE_TODO_SUCESS', payload: todo });
socket.io server: broadcast('action', {type: 'UPDATE_TODO_SUCESS', payload: todo });
Client B: on('action', dispatch({type: 'UPDATE_TODO_SUCESS', payload: todo });
middleware: emit('action', {type: 'UPDATE_TODO_SUCESS', payload: todo });
... inifinite loop!
How can I broadcast updates to other clients without causing this infinite loop and without having to handle a bunch of other action types in my reducers?
The solution I came up with is to augment the action objects with a boolean broadcast property that is used to decide if the redux-socket.io should emit the message to the server.
The second parameter of createSocketIoMiddleware can be "...a function that returns a truthy value if the action should be sent to socket.io"
client setup
let socketIoMiddleware = createSocketIoMiddleware(socket, (type, action) => {
// emit to server if action has property "broadcast: true"
return action.broadcast;
});
example action:
const addTodo = todo => {
return {
type: 'ADD_TODO_SUCCESS,
todo: todo,
broadcast: true
};
};
server setup
socket.on('action', (action) => {
// turn broadcast off so subscriber doesn't turn around and publish
if(action.broadcast) {
action.broadcast = false;
}
socket.broadcast.emit('action', action);
}
}
Related
i have another problem while using RTK Query.
We initially fetch data with a get request. Once the result added to the cache, we want add the websocket connection and listen for any changes.
But the only thing i can find in the docs is, just adding further entries via .push.
But we have to update already existing data.
The structure of the data is:
[
{
resources: event.resources, // Array
events: event.events, // Array
resourceTimeRanges: event.resourceTimeRanges, // Array
calendars: event.calendars, // Array
project: event.project, // Array
},
];
Via websocket we get changes only for the events prop of the object.
So we have to update an entry in the events array.
How would it looks like?
Our code:
async onCacheEntryAdded(arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) {
const state = store.getState();
const currentUser = state.user.uuid;
console.log(currentUser);
try {
// wait for the initial query to resolve before proceeding
await cacheDataLoaded;
// when data is received from the socket connection to the server,
// if it is a message and for the appropriate channel,
// update our query result with the received message
const listener = (event) => {
schedulerData.util.updateData('getSchedulerEvents', undefined, (draft) => {
// Test data
const newEvent = { id: 2, name: 'newName' };
// How do i update data here???
});
updateCachedData((draft) => {
console.log(draft);
});
};
// client-side
socketClient.emit('schedulerRoom', 'join');
socketClient.on('scheduler', (payload) => {
console.log('Joined room!');
console.log(payload);
listener(payload);
});
} catch (e) {
console.log(e);
// no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`,
// in which case `cacheDataLoaded` will throw
}
// cacheEntryRemoved will resolve when the cache subscription is no longer active
await cacheEntryRemoved;
// perform cleanup steps once the `cacheEntryRemoved` promise resolves
},
Thank you very much for your suggestions.
I'm using the library redux-saga-requests for network communication and high level state management. Following the guidance from the documentation, I've set up a global error handler to display a notification in case the server request times out or the user has connection troubles.
However, when I get a server error (500 for example), I can't access the response in my action. The server response is just not there.
Is there a way to access this object while still staying within the confines and rules of redux-saga?
Here's the initial store setup - note that onErrorSaga doesn't give me the network response, so I might as well go with the default behaviour - sending a suffixed _ERROR action that can be caught by my reducer:
function* onErrorSaga(error, action) {
console.log('error?', error)
console.log('action?', action)
// None of these contain the server response,
// just the error message and stack thrown by the client.
yield { error }
}
function* rootSaga(axiosInstance) {
yield createRequestInstance({
driver: {
default: createAxiosDriver(axiosInstance),
},
onError: onErrorSaga,
})
yield watchRequests()
}
const configureStore = initialState => {
const sagaMiddleware = createSagaMiddleware()
const middlewares = [thunkMiddleware, requestsPromiseMiddleware({ auto: true }), sagaMiddleware, trackingMiddleware]
const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(...middlewares)))
sagaMiddleware.run(rootSaga, axiosInstance)
return store
}
export const configureStoreAsync = () => {
return new Promise((resolve, reject) => {
const store = configureStore()
store
.dispatch(fetchAppInitialization())
.then(result => resolve(store))
.catch(e => reject(store))
})
}
Here's how I'm currently handling errors in my state (which is then displayed by a notification component):
import { CLOSE_NOTIFICATION } from '../actions/actions.notification'
export default (state = [], action) => {
if (action.type.endsWith('_ERROR')) {
return {
type: 'warning',
title: action.error.message,
message: `${action.error.stack.substring(0, 200)}...`,
open: true,
timestamp: new Date(),
}
}
switch (action.type) {
case CLOSE_NOTIFICATION:
return {
...state,
open: false,
}
default:
return state
}
}
Turns out it was way easier than I thought. The action object does in fact contain the server response.
The path to the response is:
action.error.response.data
I'm trying "redux-promise".
When there's no error in the flow, my code works properly. But, let's say that the API is down or I have a typo in the URL. In those cases, I expect to handle the error in the proper way.
This is the API: https://jsonplaceholder.typicode.com/users
(in the snippet I'm adding random text at the end to produce the 404)
Action creator
export async function fetchUsers() {
const request = await axios
.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf')
.catch(error => console.log('ERROR', error))
return {
type: FETCHING_USERS,
payload: request
};
}
Reducer
export default (state = [], action) => {
switch (action.type) {
case FETCHING_USERS:
return [...state, ...action.payload.data]
default:
return state
}
}
I can see the error logged in the console
ERROR Error: Request failed with status code 404
But, once the action is dispatched its payload is undefined
action {type: "FETCHING_USERS", payload: undefined}
I don't know where is the best place to handle this: action creator, reducer, etc. I shouldn't check if payload is something in the reducer and, if not, return state or do nothing. I want to understand which would be the best approach to handle this.
You may look at source of redux-promise, as it very simple.
Redux-promise expects either promise or action with payload set to some promise. I think you're going to use leter case.
Code may look like (just example, not tested):
export function fetchUsers() {
const request = axios.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf');
return {
type: FETCHING_USERS,
payload: request
};
}
In this case redux-promise will await for resolution of promise returned by axios.get and dispatch your action but payload replaced with promise result. In case of error, redux-promise will catch it and dispatch action with error = true (you may want to handle action.error === true case in reducer)
In the reducer you should check for the existence of the error field in action:
export default function(state = null, action) {
if (action.error) {
//handle
}
switch(action.type) {
This code is for the action.
export const fetchUsers = () => async dispatch => {
try {
const res = await axios.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf');
dispatch({
type: FETCHING_USERS,
payload: res.data
});
} catch (err) {
dispatch({
type: FETCHING_ERROR
});
}
};
In reducer do this.
Define the initial state and use this code.
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case FETCHING_USERS:
return {
...state,
loading: false,
user: payload,
};
case FETCH_ERROR:
return {
...state,
token: null,
loading: false,
};
I'm refactoring my react/redux app to use redux-observable instead of redux-thunk. Using thunk, I have an api middleware set up to listen for any actions with a CALL_API key and do some manipulation of the data, prepare headers, prepare full url, perform an api call using axios, and also do some additional action dispatches related to an api call.
Importantly, the api middleware dispatches a REQUEST_START action which gives the request an id and sets its status to pending in the network part of my state. When the promise from axios resolves or rejects, the middleware dispatches a REQUEST_END action, updating the state so that the current request is set to resolved or rejected. Then the response is returned to the calling action creator that initially dispatched the CALL_API action.
I have not been able to figure out how to do this with redux-observable. The part about the api middleware described above that I want to replicate is the REQUEST_START and REQUEST_END action dispatches. It's very convenient to have a centralized place where all api call related stuff is handled. I know I can effectively dispatch the REQUEST_START and REQUEST_END actions in each of my epics that does an api call, but I don't want to have to repeat the same code in many places.
I managed to partially solve this by creating an apiCallEpic which listens for actions with type CALL_API and does the above setup for api calls. However, an issue (or rather, something I don't like) is that the epic that initiates the api call (e.g. getCurrentUserEpic) essentially gives up control to apiCallEpic.
So, for example, when the api call succeeds and has a response, I may want to format that response data in some way before dispatching an action to be handled by my reducer. That is, getCurrentUserEpic should do some formatting of data returned from api call before sending to reducer. I was able to achieve something close to this by passing a payloadHandler callback function defined in getCurrentUserEpic that the apiCallEpic can call if/when it gets a successful response. However, I don't like this callback architecture and it seems like there's got to be a better way.
Here is some code that demonstrates my use of api middleware using thunk.
import axios from 'axios';
// actionCreators.js
// action types
const CALL_API = "CALL_API";
const FETCH_CURRENT_USER = "FETCH_CURRENT_USER";
const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER";
// action creators for request start and end
export const reqStart = (params = {}) => (dispatch) => {
const reduxAction = {
type: REQ_START,
status: 'pending',
statusCode: null,
requestId: params.requestId,
}
dispatch(reduxAction);
}
export const reqEnd = (params = {}) => (dispatch) => {
const {
requestId,
response = null,
error = null,
} = params;
let reduxAction = {}
if (response) {
reduxAction = {
type: REQ_END,
status: 'success',
statusCode: response.status,
requestId,
}
}
else if (error) {
if (error.response) {
reduxAction = {
type: REQ_END,
status: 'failed',
statusCode: error.response.status,
requestId,
}
}
else {
reduxAction = {
type: REQ_END,
status: 'failed',
statusCode: 500,
requestId,
}
}
}
dispatch(reduxAction);
}
// some api call to fetch data
export const fetchCurrentUser = (params = {}) => (dispatch) => {
const config = {
url: '/current_user',
method: 'get',
}
const apiCall = {
[CALL_API]: {
config,
requestId: FETCH_CURRENT_USER,
}
}
return dispatch(apiCall)
.then(response => {
dispatch({
type: RECEIVE_CURRENT_USER,
payload: {response},
})
return Promise.resolve({response});
})
.catch(error => {
return Promise.reject({error});
})
}
// apiMiddleware.js
// api endpoint
const API_ENTRY = "https://my-api.com";
// utility functions for request preparation
export const makeFullUrl = (params) => {
// ...prepend endpoint url with API_ENTRY constant
return fullUrl
}
export const makeHeaders = (params) => {
// ...add auth token to headers, etc.
return headers;
}
export default store => next => action => {
const call = action[CALL_API];
if (call === undefined) {
return next(action);
}
const requestId = call.requestId;
store.dispatch(reqStart({requestId}));
const config = {
...call.config,
url: makeFullUrl(call.config),
headers: makeHeaders(call.config);
}
return axios(config)
.then(response => {
store.dispatch(reqEnd({
response,
requestId,
}))
return Promise.resolve(response);
})
.catch(error => {
store.dispatch(reqEnd({
error,
requestId,
}))
return Promise.reject(error);
})
}
// reducers.js
// Not included, but you can imagine reducers handle the
// above defined action types and update the state
// accordingly. Most usefully, components can always
// subscribe to specific api calls and check the request
// status. Showing loading indicators is one
// use case.
Here's the code I've implemented to accomplish a similar thing with redux-observable.
export const fetchCurrentUserEpic = (action$, state$) => {
const requestType = FETCH_CURRENT_USER;
const successType = RECEIVE_CURRENT_USER;
const requestConfig = {
url: "/current_user",
method: "get",
}
const payload = {requestConfig, requestType, successType};
const payloadNormalizer = ({response}) => {
return {currentUser: response.data.data};
}
return action$.ofType(FETCH_CURRENT_USER).pipe(
switchMap((action) => of({
type: CALL_API,
payload: {...payload, requestId: action.requestId, shouldFail: action.shouldFail, payloadNormalizer},
})),
)
}
export const apiEpic = (action$, state$) => {
return action$.ofType(CALL_API).pipe(
mergeMap((action) => (
concat(
of({type: REQ_START, payload: {requestId: action.payload.requestId, requestType: action.payload.requestType}}),
from(callApi(action.payload.requestConfig, action.payload.shouldFail)).pipe(
map(response => {
return {
type: action.payload.successType,
payload: action.payload.payloadNormalizer({response})
}
}),
map(() => {
return {
type: REQ_END,
payload: {status: 'success', requestId: action.payload.requestId, requestType: action.payload.requestType},
}
})
)
)
).pipe(
catchError(error => {
console.log('error', error);
return of({type: REQ_END, payload: {status: 'failed', requestId: action.payload.requestId, requestType: action.payload.requestType}, error});
})
)
)
)
}
Any comments or suggestions are appreciated!
I've found redux-fetch-epic-builder (A lib for building "fetch actions" and generic epics handled by redux-observable) to be similar to what you are trying to achieve here (beware it uses rxjs 5, this guide to rescue). It uses fetch, not axios, but it's easy to replace that. Plus it has transformers for successful/failed actions.
The library is a bit old, but the base idea to overcome boilerplate code is still valid: Generic epic-builder to fetch data with calls to API(s).
I am a novice in React / Redux / RxJS, but the only problem I see with the redux-fetch-epic-builder is the way to configure the client (in axios terms). That is, I am not fully satisfied with (due to it being not FSA or RSAA):
//action creators
const getComments = (id, page = 1) => ({
type: GET_COMMENTS,
host: 'http://myblog.com',
path: `/posts/${id}/comments`,
query: {
page,
},
})
// ...
const epics = [
buildEpic(GET_COMMENTS),
]
but this may still be an elegant way. And the license allow to develop the library further. I have not converted the example from the library documentation to your user-related example, but with react-observable there is certainly no need to introduce a separate "api middleware". (Also, I like /SUBACTION better than _SUBACTION, but it's trivial to change.)
I have an action, that uses a redux thunk, that looks like so:
export function fetchData(query) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(someOtherAction(json)) })
}
}
}
and then my someOtherAction actually updates state:
export function someOtherAction(data) {
return {
action: types.SOME_ACTION,
data
}
}
But i want it to be possible for the fetchData action creator to be reusable so that different parts of my app can fetch data from myapi and then have different parts of the state based on that.
I'm wondering what is the best way to reuse this action? Is it acceptable to pass a second parameter in to my fetchData action creator that stipulates which action is called on a successful fetch:
export function fetchData(query, nextAction) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(nextAction(json)) })
}
}
}
Or is there an accepted way of doing this sort of thing?
I use a middleware for that. I have defined the fetch call in there, then in my actions I send the URL to fetch and the actions to dispatch when completed. This would be a typical fetch action:
const POSTS_LOAD = 'myapp/POST_L';
const POST_SUCCESS = 'myapp/POST_S';
const POST_FAIL = 'myapp/POST_F';
export function fetchLatestPosts(page) {
return {
actions: [POSTS_LOAD, POST_SUCCESS, POST_FAIL],
promise: {
url: '/some/path/to/posts',
params: { ... },
headers: { ... },
},
};
}
When calling that action, the POST_LOAD action will be dispatch automatically by the middleware just before the fetch request it's executed. If everything goes well the POST_SUCCESS action will be dispatched with the json response, if something goes wrong the POST_FAIL action will be dispatched by the middleware.
All the magic it's in the middleware! And it's something similar to this:
export default function fetchMiddleware() {
return ({ dispatch, getState }) => {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, actions, ...rest } = action;
if (!promise) {
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = actions;
next({ ...rest, type: REQUEST }); // <-- dispatch the LOAD action
const actionPromise = fetch(promise.url, promise); // <-- Make sure to add the domain
actionPromise
.then(response => response.json())
.then(json => next({ ...rest, json, type: SUCCESS })) // <-- Dispatch the success action
.catch(error => next({ ...rest, error, type: FAILURE })); // <-- Dispatch the failure action
return actionPromise;
};
};
}
This way I have all my requests on a single place and I can define the actions to run after the request it's completed.
------------EDIT----------------
In order to get the data on the reducer, you need to use the action name you defined on the original action creator. The following example shows how to handle the POST_SUCCESS action from the middleware to get the posts data from the json response.
export function reducer(state = {}, action) {
switch(action.type) {
case POST_SUCCESS: // <-- Action name
return {
...state,
posts: action.json.posts, // <-- Getting the data from the action
}
default:
return state;
}
}
I hope this helps!