Request failed with status code 404 (REDUX) - reactjs

Can't seem to pull data from database but according Redux DevTools the actions PASSWORD_LIST_REQUEST seems to be firing. What is going wrong here? I checked my original route, it should be going towards "api/passwords" in the server, is this a backend problem or Redux problem?
I checked the backend url routes with postman, so thats all good.
ACTIONS
export const listPasswords = () => async (dispatch) => {
try {
dispatch({ type: PASSWORD_LIST_REQUEST });
const { data } = await axios.get("/api/passwords");
dispatch({
type: PASSWORD_LIST_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PASSWORD_LIST_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
REDUCERS
export const passwordListReducer = (state = { passwords: [] }, action) => {
switch (action.type) {
case PASSWORD_LIST_REQUEST:
return {
loading: true,
passwords: [],
};
case PASSWORD_LIST_SUCCESS:
return { passwords: action.payload, loading: false };
case PASSWORD_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
STORE
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import { passwordListReducer } from "./reducers/passwordReducers";
const reducer = combineReducers({ passwordList: passwordListReducer });
const initialState = {};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
Server
import passwordRoutes from "./routes/passwordRoutes.js";
dotenv.config();
connectDB();
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.get("/", (req, res) => {
res.send("API is running");
});
app.use("/api/passwords", passwordRoutes);
app.use(notFound);
app.use(errorHandler);
const PORT = process.env.PORT || 5000;
app.listen(
PORT,
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`)
);

Seems like your call to endpoint might be going to the wrong url here
const { data } = await axios.get("/api/passwords");
The above axios call would point to your app url, say you app is running on localhost:3000, an API call would be made to http://localhost:3000/api/password by default.
If your app and back end run on different ports on your localhost, you have to set the domain on the axios call. From you code sample, I see you set you APIs to run from port 5000.
const { data } = await axios.get("http://localhost:5000/api/passwords");
Tip
If you don't want to keep repeating the base url, you can create an axios instance
export const request = axios.create({baseUrl: "http://localhost:5000"});
Then make your requests this way
import { request } from "path to request module";
...
const { data } = await request.get("/api/passwords");
...

I needed a proxy in package.json.
"proxy": "http://localhost:5000",

Related

Request URL from React (Vite) Front-End to Express.js Back-End includes page path and results 404 Not Found

I created an Express.js Back-End that runs on port 4000. Also React (Vite) app runs on port 5173. I try to make some axios requests to my Back-End. Eventhough the URL looks wrong on DevTools when I make any request from my home page, it is still able to hit the Back-End and fetch the data (I can log the request on the Back-End console). But when I try to make a request from another page such as "127.0.0.1:5173/quiz", the request URL also includes "quiz". That's why I get 404.
So it shows "http://127.0.0.1:5173/quiz/api/quiz/:quizId"
But it needs to be "http://127.0.0.1:4000/api/quiz/:quizId"
But like I said, it works when I make a request on home page:
"http://127.0.0.1:5173/api/quiz" - This works, and fetches the quiz list.
Btw, to solve CORS issues, I tried to add "proxy" on package.json, but it didn't work. Then I add some configurations on vite.config.ts, it worked, but like I said I kept seeing "http://127.0.0.1:5173" on Dev Tools Network tab instead of "http://127.0.0.1:4000".
Here's my vite.config.ts:
import { defineConfig } from "vite";
import react from "#vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
secure: false,
},
},
},
});
Here's my request code
import { useState } from "react";
import { useAuthContext } from "./useAuthContext";
import { useQuizContext } from "./useQuizContext";
import { QuizType, Types as QuizActionTypes } from "../context/quiz/types";
import axios from "axios";
export const useQuiz = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { state: authState } = useAuthContext();
const { dispatch } = useQuizContext();
const getQuiz = async (id: string) => {
setIsLoading(true);
setError(null);
const queryParams: string[] = [];
// If it's a logged in user
if (authState.user.token) {
queryParams.push(`userId=${authState.user._id}`);
}
// If it's a participant who's done with the quiz
if (localStorage.getItem("gifQuizUser")) {
queryParams.push("participantCompleted=true");
}
const uri =
queryParams.length > 0
? `api/quiz/${id}?${queryParams.join("&")}`
: `api/quiz/${id}`;
console.log(uri);
// Fetch & Dispatch
try {
const response = await axios.get(uri);
if (!response.data.error) {
dispatch({
type: QuizActionTypes.GetQuiz,
payload: response.data,
});
setIsLoading(false);
}
} catch (err: any) {
if (err.response.data.error) {
setIsLoading(false);
setError(err.response.data.error);
}
}
};
return { isLoading, error, getQuizzes, getQuiz };
};
Thank you for your time.

Using keycloak public client token to communicate with confidential client

I have a quarkus backend app with a react frontend. I want to add a security layer where a user has to login in order to be able to access the UI, and any API calls made from the UI to the backend requires a token with the user is authenticated. Keycloak is the best and simplest(ish) solution for this.
I found this tutorial and it's exactly what I need but doesn't work :(
My keycloak setup is a client for the frontend and backend.
Frontend Setup
The frontend client is a public access type
I then defined a few different JS files to setup the connection to keycloak and the header token...
UserService.ts
import Keycloak, {KeycloakInstance} from 'keycloak-js'
const keycloak: KeycloakInstance = Keycloak('/keycloak.json');
/**
* Initializes Keycloak instance and calls the provided callback function if successfully authenticated.
*
* #param onAuthenticatedCallback
*/
const initKeycloak = (onAuthenticatedCallback) => {
keycloak.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + /silent-check-sso.html,
checkLoginIframe: false,
})
.then((authenticated) => {
if (authenticated) {
onAuthenticatedCallback();
} else {
doLogin();
}
})
};
const doLogin = keycloak.login;
const doLogout = keycloak.logout;
const getToken = () => keycloak.token;
const getKeycloakId = () => keycloak.subject; // subject is the keycloak id
const isLoggedIn = () => !!keycloak.token;
const updateToken = (successCallback) =>
keycloak.updateToken(5)
.then(successCallback)
.catch(doLogin);
const getUserInfo = async () => await keycloak.loadUserInfo();
const getUsername = () => keycloak.tokenParsed?.sub;
const hasRole = (roles) => roles.some((role) => keycloak.hasResourceRole(role, 'frontend'));
export const UserService = {
initKeycloak,
doLogin,
doLogout,
isLoggedIn,
getToken,
getKeycloakId,
updateToken,
getUsername,
hasRole,
};
keycloak.json
{
"realm": "buddydata",
"auth-server-url": "http://127.0.0.1:8180/auth/",
"ssl-required": "external",
"resource": "app",
"public-client": true,
"verify-token-audience": true,
"use-resource-role-mappings": true,
"confidential-port": 0
}
HttpService.ts
import axios from "axios";
import {UserService} from "#/services/UserService";
const HttpMethods = {
GET: 'GET',
POST: 'POST',
DELETE: 'DELETE',
PUT: 'PUT',
};
const baseURL = 'http://localhost:9000/api/v1';
const _axios = axios.create({baseURL: baseURL});
const configure = () => {
_axios.interceptors.request.use((config) => {
if (UserService.isLoggedIn()) {
const cb = () => {
config.headers.Authorization = `Bearer ${UserService.getToken()}`;
return Promise.resolve(config);
};
return UserService.updateToken(cb);
}
});
};
const getAxiosClient = () => _axios;
export const HttpService = {
HttpMethods,
configure,
getAxiosClient
};
Backend Setup
There is no need to define any code updates, just config updates for quarkus...
application.properties
quarkus.resteasy-reactive.path=/api/v1
quarkus.http.port=9000
quarkus.http.cors=true
quarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/buddydata
quarkus.oidc.client-id=api
quarkus.oidc.credentials.secret=ff5b3f63-446f-4ca4-8623-1475cb59a343
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated
That is the keycloak config setup along with quarkus and react setup too. I have included all the information to show it all matches up. When the user logs in on the frontend a call is made to the backend to get the initial state, pre oidc setup, this worked but now it doesn't.
Root.tsx
const store = StoreService.setup();
export const Root = (): JSX.Element => {
StoreService.getInitialData(store)
.then(_ => console.log("Initial state loaded"));
return (
<RenderOnAuthenticated>
<h1>Hello {UserService.getUsername()}</h1>
</RenderOnAuthenticated>
)
};
StoreService.ts
const setup = () => {
const enhancers = [];
const middleware = [
thunk,
axiosMiddleware(HttpService.getAxiosClient())
];
if (process.env.NODE_ENV === 'development') {
enhancers.push(applyMiddleware(logger));
}
// const composedEnhancers = compose(applyMiddleware(...middleware), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), ...enhancers);
const composedEnhancers = compose(applyMiddleware(...middleware), ...enhancers);
return createStore(rootReducer, composedEnhancers);
};
const getInitialData = async store => {
// Get the logged in user first
await store.dispatch(getLoggedInUser());
const user = selectCurrentUser(store.getState());
}
const StoreService = {
setup,
getInitialData
};
export default StoreService;
currentUser.ts
const axios = HttpService.getAxiosClient();
// User state interface
export interface UserState {
id: number
keycloakId: string,
address?: {},
title?: string,
firstName?: string,
lastName?: string,
jobTitle?: string,
dateOfBirth?: string,
mobile?: string,
email: string,
isAdmin?: boolean,
creationDate?: Date,
updatedDate?: Date
}
// Action Types
const GET_USER_SUCCESS = 'currentUser/GET_USER_SUCCESS';
// Reducer
const initialUser: UserState = {id: -1, keycloakId: "-1", email: "no#email"}
export const currentUserReducer = (currentUserState = initialUser, action) => {
switch (action.type) {
case GET_USER_SUCCESS:
return {...currentUserState, ...action.payload.user};
default:
return currentUserState;
}
};
// Synchronous action creator
export const getLoggedInUserSuccess = userResponse => ({
type: GET_USER_SUCCESS,
payload: { user: userResponse.data }
})
// Asynchronous thunk action creator
// calls api, then dispatches the synchronous action creator
export const getLoggedInUser = (keycloakId = UserService.getKeycloakId()) => {
return async getLoggedInUser => {
try {
let axiosResponse = await axios.get(`/initial/users/${keycloakId}`)
getLoggedInUser(getLoggedInUserSuccess(axiosResponse))
} catch(e) {
console.log(e);
}
}
}
// Selectors
export const selectCurrentUser = state => state.currentUser;
InitialDataController.java
#Path("/initial")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class InitialDataController {
#Inject
UserService userService;
#GET
#Path("/users/{keycloakId}")
public Response getUserByKeycloakId(#PathParam("keycloakId") String keycloakId) {
UserEntity user = userService.getUserWithKeycloakId(keycloakId);
return Response
.ok(user)
.build();
}
}
UserService.java
#ApplicationScoped
public class UserService {
#Inject
UserRepository repository;
public UserEntity getUserWithKeycloakId(#NotNull String keycloakId) {
return repository.findUserWithKeycloakId(keycloakId);
}
}
UserRepository.java
#ApplicationScoped
public class UserRepository implements PanacheRepositoryBase<UserEntity, Long> {
public UserEntity findUserWithKeycloakId(String keycloakId) {
return find("#User.getUserByKeycloakId", keycloakId).firstResult();
}
}
The named query is SELECT u FROM User u WHERE u.keycloakId = ?1
I have provided my config setup for keycloak, react and quarkus and a step through the code on how calls are made to the backend from the UI.
How can I secure the backend client on keycloak and have the frontend public, and then be able to make secure requests from the frontend to the backend. At the moment any requests made is giving a 401 Unauthorized response. I don't know how to get around this and I feel it's a keycloak config issue but not sure which option to change/update specifically. Any new knowledge/information on how to get over this would be great.

How to logout automatically when session expires while using createAsyncThunk and axios (withcredential) option using react and redux toolkit?

I am trying to logout the user when the session expires after a certain period of time. I am using redux-toolkit with react for my API calls and, hence, using the createAsyncThunk middleware for doing so.
I have around 60 API calls made in maybe 20 slices throughout my application. Also, there is a async function for logout too that is fired up on the button click. Now the problem that I am facing is that if the session expires, I am not able to logout the user automatically. If I had to give him the message, then I had to take up that message from every api call and make sure that every screen of mine has a logic to notify the Unautherised message.
I did check a method called Polling that calls an API after a certain given time. And I believe that this is not a very efficient way to handle this problem.
**Here is a little code that will help you understand how my API calls are being made in the slices of my application. **
// Here is the custom created api that has axios and withcredentials value
import axios from "axios";
const api = axios.create({
baseURL:
process.env.NODE_ENV === "development" ? process.env.REACT_APP_BASEURL : "",
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
export default api;
// My Logout Function!!
export const logoutUser = createAsyncThunk(
"userSlice/logoutUser",
async (thunkAPI) => {
try {
const response = await api.get("/api/admin/logout");
if (response.status === 200) {
return response.data;
} else {
return thunkAPI.rejectWithValue(response.data);
}
} catch (e) {
return thunkAPI.rejectWithValue(e.response.data);
}
}
);
I want to dispatch this function whenever there is a response status-code is 401 - Unauthorised. But I don't want to keep redundant code for all my other API calls calling this function. If there is a middleware that might help handle this, that would be great, or any solution will be fine.
// Rest of the APIs are called in this way.
..........
export const getStatus = createAsyncThunk(
"orgStat/getStatus",
async (thunkAPI) => {
try {
const response = await api.get("/api/admin/orgstat");
if (response.status === 200) {
return response.data;
} else {
return thunkAPI.rejectWithValue(response.data);
}
} catch (e) {
return thunkAPI.rejectWithValue(e.response.data);
}
}
);
const OrgStatusSlice = createSlice({
name: "orgStat",
initialState,
reducers: {
.......
},
extraReducers: {
[getStatus.pending]: (state) => {
state.isFetching = true;
},
[getStatus.rejected]: (state, { payload }) => {
state.isFetching = false;
state.isError = true;
state.isMessage = payload.message;
},
[getStatus.fulfilled]: (state, { payload }) => {
state.isFetching = false;
state.data = payload.data;
},
},
});
.......
If needed any more clearence please comment I will edit the post with the same.
Thank You!!
import axios from 'axios'
import errorParser from '../services/errorParser'
import toast from 'react-hot-toast'
import {BaseQueryFn} from '#reduxjs/toolkit/query'
import {baseQueryType} from './apiService/types/types'
import store from './store'
import {handleAuth} from './common/commonSlice'
import storageService from '#services/storageService'
// let controller = new AbortController()
export const axiosBaseQuery =
(
{baseUrl}: {baseUrl: string} = {baseUrl: ''}
): BaseQueryFn<baseQueryType, unknown, unknown> =>
async ({url, method, data, csrf, params}) => {
const API = axios.create({
baseURL: baseUrl,
})
API.interceptors.response.use(
(res) => {
if (
res.data?.responseCode === 1023 ||
res.data?.responseCode === 6023
) {
if(res.data?.responseCode === 1023){
console.log('session expired')
store.dispatch(handleSession(false))
return
}
console.log('Lopgged in somewhere else')
store.dispatch(handleSession(false))
storageService.clearStorage()
// store.dispatch(baseSliceWithTags.util.resetApiState())
return
// }, 1000)
}
return res
},
(error) => {
const expectedError =
error.response?.status >= 400 &&
error.response?.status < 500
if (!expectedError) {
if (error?.message !== 'canceled') {
toast.error('An unexpected error occurrred.')
}
}
if (error.response?.status === 401) {
// Storage.clearJWTToken();
// window.location.assign('/')
}
return Promise.reject(error)
}
)
try {
let headers = {}
if (csrf) headers = {...csrf}
const result = await API({
url: url,
method,
data,
headers,
params: params ? params : '',
baseURL: baseUrl,
// signal: controller.signal,
})
return {data: result.data}
} catch (axiosError) {
const err: any = axiosError
return {
error: {
status: errorParser.parseError(err.response?.status),
data: err.response?.data,
},
}
}
}
I am also using RTK with Axios. You can refer to the attached image.

server side redux-saga initial state

I'm using react-boilerplate for my App (Using SSR branch). For some reason we need server side rendering. Also we have some rest API and we need to call one API before all API (for register something). I thinks for initial state I need to call this (first API that need data for registration ) API on the server and save response data into store and return store to client. In react-boilerplate for create store:
/**
* Create the store with asynchronously loaded reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';
const sagaMiddleware = createSagaMiddleware();
export default function configureStore(initialState = {}, history) {
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [
sagaMiddleware,
routerMiddleware(history),
];
const enhancers = [
applyMiddleware(...middlewares),
];
// If Redux DevTools Extension is installed use it, otherwise use Redux compose
/* eslint-disable no-underscore-dangle */
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;
/* eslint-enable */
const store = createStore(
createReducer(),
fromJS(initialState),
composeEnhancers(...enhancers)
);
// Extensions
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {}; // Async reducer registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
import('./reducers').then((reducerModule) => {
const createReducers = reducerModule.default;
const nextReducers = createReducers(store.asyncReducers);
store.replaceReducer(nextReducers);
});
});
}
return store;
}
and also for making initial store defined :
function renderAppToStringAtLocation(url, { webpackDllNames = [], assets, lang }, callback) {
const memHistory = createMemoryHistory(url);
const store = createStore({}, memHistory);
syncHistoryWithStore(memHistory, store);
const routes = createRoutes(store);
const sagasDone = monitorSagas(store);
store.dispatch(changeLocale(lang));
match({ routes, location: url }, (error, redirectLocation, renderProps) => {
if (error) {
callback({ error });
} else if (redirectLocation) {
callback({ redirectLocation: redirectLocation.pathname + redirectLocation.search });
} else if (renderProps) {
renderHtmlDocument({ store, renderProps, sagasDone, assets, webpackDllNames })
.then((html) => {
const notFound = is404(renderProps.routes);
callback({ html, notFound });
})
.catch((e) => callback({ error: e }));
} else {
callback({ error: new Error('Unknown error') });
}
});
}
and for filling the initial state I do some change:
async function fetches (hostname) {
const domain = hostname.replace('.myExample.com', '').replace('www.', '');
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/example.api.v1.0+json',
}
};
const uri ='https://api.example.com/x/' + domain + '/details';
const shopDetail = await fetch(uri, options);
return shopDetail.json();
}
function renderAppToStringAtLocation(hostname ,url, { webpackDllNames = [], assets, lang }, callback) {
const memHistory = createMemoryHistory(url);
console.log('url :', hostname);
fetches(hostname).then( data => {
const store = createStore(data, memHistory);
syncHistoryWithStore(memHistory, store);
const routes = createRoutes(store);
const sagasDone = monitorSagas(store);
store.dispatch(changeLocale(lang));
match({ routes, location: url }, (error, redirectLocation, renderProps) => {
if (error) {
callback({ error });
} else if (redirectLocation) {
callback({ redirectLocation: redirectLocation.pathname + redirectLocation.search });
} else if (renderProps) {
renderHtmlDocument({ store, renderProps, sagasDone, assets, webpackDllNames })
.then((html) => {
const notFound = is404(renderProps.routes);
callback({ html, notFound });
})
.catch((e) => callback({ error: e }));
} else {
callback({ error: new Error('Unknown error') });
}
});
});
and then in console I get this error:
Unexpected properties "code", "data" found in initialState argument
passed to createStore. Expected to find one of the known reducer
property names instead: "route", "global", "language". Unexpected
properties will be ignored.
how to fix it?
I thinks for initial state I need to call this (first API that need data for registration ) API on the server and save response data into store and return store to client
There are two different solutions, dependent on side, on which API call should be performed.
If it's just server-side call, HTTP response and subsequent SSR phase should be delayed, until fetch is done. It can be solved in express by wrapping into middleware function. Usually such schema is used when integrating with external authorization services (Auth0, Passport, etc), but it's better to wrap authorization information into JWT and not into INITIAL_STATE.
If API call can be done from client side, just use redux-saga. It can spawn dedicated process, which will catch all redux actions before API call is done, and then play them respectively. In this case initialState object should contain structure-like fields without data, which will be filled later after API call.

How to store client in redux?

I'm setting up a redux application that needs to create a client. After initialization, the client has listeners and and APIs that will need to be called based on certain actions.
Because of that I need to keep an instance of the client around. Right now, I'm saving that in the state. Is that right?
So I have the following redux action creators, but then when I want to send a message I need to call the client.say(...) API.
But where should I get the client object from? Should I retrieve the client object from the state? My understanding is that that's a redux anti-pattern. What's the proper way to do this with redux?
Even stranger – should the message send be considered an action creator when it doesn't actually mutate the state?
The actions:
// actions.js
import irc from 'irc';
export const CLIENT_INITIALIZE = 'CLIENT_INITIALIZE';
export const CLIENT_MESSAGE_RECEIVED = 'CLIENT_MESSAGE_RECEIVED';
export const CLIENT_MESSAGE_SEND = 'CLIENT_MESSAGE_SEND';
export function messageReceived(from, to, body) {
return {
type: CLIENT_MESSAGE_RECEIVED,
from: from,
to: to,
body: body,
};
};
export function clientSendMessage(to, body) {
client.say(...); // <--- where to get client from?
return {
type: CLIENT_MESSAGE_SEND,
to: to,
body: body,
};
};
export function clientInitialize() {
return (dispatch) => {
const client = new irc.Client('chat.freenode.net', 'react');
dispatch({
type: CLIENT_INITIALIZE,
client: client,
});
client.addListener('message', (from, to, body) => {
console.log(body);
dispatch(messageReceived(from, to, body));
});
};
};
And here is the reducer:
// reducer.js
import { CLIENT_MESSAGE_RECEIVED, CLIENT_INITIALIZE } from '../actions/client';
import irc from 'irc';
export default function client(state: Object = { client: null, channels: {} }, action: Object) {
switch (action.type) {
case CLIENT_MESSAGE_RECEIVED:
return {
...state,
channels: {
...state.channels,
[action.to]: [
// an array of messages
...state.channels[action.to],
// append new message
{
to: action.to,
from: action.from,
body: action.body,
}
]
}
};
case CLIENT_JOIN_CHANNEL:
return {
...state,
channels: {
...state.channels,
[action.channel]: [],
}
};
case CLIENT_INITIALIZE:
return {
...state,
client: action.client,
};
default:
return state;
}
}
Use middleware to inject the client object into action creators! :)
export default function clientMiddleware(client) {
return ({ dispatch, getState }) => {
return next => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, ...rest } = action;
if (!promise) {
return next(action);
}
next({ ...rest });
const actionPromise = promise(client);
actionPromise.then(
result => next({ ...rest, result }),
error => next({ ...rest, error }),
).catch((error) => {
console.error('MIDDLEWARE ERROR:', error);
next({ ...rest, error });
});
return actionPromise;
};
};
}
Then apply it:
const client = new MyClient();
const store = createStore(
combineReducers({
...
}),
applyMiddleware(clientMiddleware(client))
);
Then you can use it in action creators:
export function actionCreator() {
return {
promise: client => {
return client.doSomethingPromisey();
}
};
}
This is mostly adapted from the react-redux-universal-hot-example boilerplate project. I removed the abstraction that lets you define start, success and fail actions, which is used to create this abstraction in action creators.
If your client is not asynchronous, you can adapt this code to simply pass in the client, similar to how redux-thunk passes in dispatch.

Resources