React Redux state undefined in mapStateToProps - react-redux

I am having trouble with state being undefined in mapStateToProps in my React Redux app. The only way I can get it to work is to use store.getState(), but I know this is not the correct way to do it. If I log the state to the console, I get undefined (and if I try to map state to properties, I also get undefined). I am stumped as to why this is happening. I feel like I am missing something fundamental, but I just can't put my finger on it. I know using Redux would not be practical normally for a simple app like this, but I just wanted to practise using it. Any help would be appreciated!
This is my mapStateToProps function:
const mapStateToProps = (state) => {
console.log(state); //Returns undefined
tempState = store.getState(); //Why do I have to do this?
return {
text: tempState.reduxText,
author: tempState.reduxAuthor,
backgroundColor: tempState.reduxBackgroundColor,
buttonColor: tempState.reduxButtonColor,
textColor: tempState.reduxTextColor,
tweetURL: tempState.reduxTweetURL,
textFade: tempState.reduxTextFade
}
}
These are my actions and action creators:
const CHANGE_QUOTE_ACTION = 'CHANGE_QUOTE_ACTION';
const CHANGE_QUOTE_TRANSITION = 'CHANGE_QUOTE_TRANSITION';
const changeQuoteActionCreator = () => {
return {
type: CHANGE_QUOTE_ACTION,
payload: {
reduxText: quoteArr[quoteRandomIndex][0],
reduxAuthor: quoteArr[quoteRandomIndex][1],
reduxTweetURL: 'https://twitter.com/intent/tweet?text=\"' + quoteArr[quoteRandomIndex][0] + '\" ' + quoteArr[quoteRandomIndex][1],
reduxTextFade: 'textVisible '
}
}
}
const changeQuoteTransitionActionCreator = () => {
while (quoteArr[quoteRandomIndex][0] == store.getState().reduxText) {
quoteRandomIndex = Math.floor(Math.random() * 10);
}
while ('container-fluid ' + colorArr[colorRandomIndex] + '-color' == store.getState().reduxBackgroundColor) {
colorRandomIndex = Math.floor(Math.random() * 8);
};
return {
type: CHANGE_QUOTE_TRANSITION,
payload: {
reduxBackgroundColor: 'container-fluid ' + colorArr[colorRandomIndex] + '-color',
reduxButtonColor: 'btn btn-outline shadow-none text-white ' + colorArr[colorRandomIndex] + '-color ' + colorArr[colorRandomIndex] + '-hoverColor',
reduxTextColor: colorArr[colorRandomIndex] + '-textColor',
reduxTextFade: 'textInvisible'
}
}
}
const asyncChangeQuoteActionCreator = () => {
return function (dispatch) {
dispatch(changeQuoteTransitionActionCreator());
setTimeout(function () {
dispatch(changeQuoteActionCreator());
}, 1000);
};
}
This is my reducer:
initialReduxState = {
reduxText: '',
reduxAuthor: '',
reduxBackgroundColor: 'container-fluid ' + colorArr[initialColorRandomIndex] + '-color',
reduxButtonColor: 'btn btn-outline shadow-none text-white ' + colorArr[initialColorRandomIndex] + '-color ' + colorArr[initialColorRandomIndex] + '-hoverColor',
reduxTextColor: colorArr[initialColorRandomIndex] + '-textColor',
reduxTweetURL: ''
}
//Redux reducer
const changeQuoteReducer = (state = initialReduxState, action) => {
switch (action.type) {
case CHANGE_QUOTE_ACTION:
return Object.assign({}, state, { reduxText: action.payload.reduxText, reduxAuthor: action.payload.reduxAuthor, reduxTweetURL: action.payload.reduxTweetURL, reduxTextFade: action.payload.reduxTextFade });
case CHANGE_QUOTE_TRANSITION:
return Object.assign({}, state, { reduxBackgroundColor: action.payload.reduxBackgroundColor, reduxButtonColor: action.payload.reduxButtonColor, reduxTextColor: action.payload.reduxTextColor, reduxTextFade: action.payload.reduxTextFade })
default:
return state;
}
}
This is connecting to the store:
const store = Redux.createStore(
changeQuoteReducer,
Redux.applyMiddleware(ReduxThunk.default)
);
const Provider = ReactRedux.Provider;
const connect = ReactRedux.connect;
This is mapDispatchToProps:
const mapDispatchToProps = (dispatch) => {
return {
changeQuoteDispatch: () => {
dispatch(asyncChangeQuoteActionCreator())
}
}
}
Subscribing to the store, dispatching to the store and connecting Redux to React:
store.subscribe(mapStateToProps);
store.dispatch(mapDispatchToProps);
const Container = connect(mapStateToProps, mapDispatchToProps)(Presentational);

Related

How to use React useContext with leaflet routing machine and react leaflet?

I'm trying to use a useContext hook inside a react-leaflet controlComponent but I have an error when my context fires the update function.
I use a react-leaflet controlComponent because of leaflet routing machine. I think the code + the error are better than word:
MainBoard.tsx
export const CartographyContext: React.Context<CartographyContextType> = React.createContext<CartographyContextType>({ positions: [] });
...
const routeSummaryValueContext = React.useMemo(
() => ({ routeSummary, setRouteSummary }),
[routeSummary]
);
const elevationProfileValueContext = React.useMemo(
() => ({ elevationProfile, setElevationProfile }),
[elevationProfile]
);
........
<CartographyContext.Provider value={{ positions, elevationProfileValueContext, routeSummaryValueContext, positionsValueContext, addPosition, changePosition }}>
.........
<RoutingMachine
orsOptions={{
....
}} />
..........
</CartographyContext.Provider>
RoutingMachine.tsx:
const CreateRoutineMachineLayer = (props: any) => {
const geoService = new GeoLocalisationService();
const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
const [routes, setRoutes] = React.useState<any[]>();
React.useEffect(() => {
if (routes) {
//The line which cause the error
cartographyContext.elevationProfileValueContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
const summary: RouteSummary = {
ascent: routes[0].routeSummary.ascent,
descent: routes[0].routeSummary.descent,
distance: routes[0].routeSummary.distance,
estimatedDuration: routes[0].routeSummary.duration
}
cartographyContext.routeSummaryValueContext.setRouteSummary(summary);
}
}, [routes]);
const { orsOptions } = props;
const instance = L.Routing.control({
router: new OpenRouteRouter(orsOptions),
lineOptions: {
styles: [{ color: "#3933ff", weight: 4 }],
extendToWaypoints: true,
missingRouteTolerance: 0
},
routeWhileDragging: true,
autoRoute: true,
geocoder: new geocoder.Geocoder(),
}).on('routesfound', (e) => {
setRoutes(e.routes);
});
useMapEvents({
click: (e: L.LeafletMouseEvent) => {
if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else {
instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
}
}
});
return instance;
};
const RoutingMachine = createControlComponent(CreateRoutineMachineLayer);
error :
g: React has detected a change in the order of Hooks called by ForwardRef(LeafComponent). This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useContext useContext
2. useRef useRef
3. useContext useRef
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..............
Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I clearly doing something wrong here but I haven't found yet.
Thank you
Kind regards
Ok I found the good implementation :
const RoutingMachine: React.FC<RoutingMachineProps> = (props) => {
//const RoutineMachine = (props: any) => {
const geoService = new GeoLocalisationService();
const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
const [instance, setInstance] = React.useState<any>();
const [alreadyDisplayed, setAlreadyDisplayed] = React.useState(false);
const { orsOptions } = props;
const map = useMap();
//const instance = L.Routing.control({
React.useEffect(() => {
const instance = L.Routing.control({
router: new OpenRouteRouter(orsOptions),
lineOptions: {
styles: [{ color: "#3933ff", weight: 4 }],
extendToWaypoints: true,
missingRouteTolerance: 0
},
routeWhileDragging: true,
autoRoute: true,
geocoder: (L.Control as any).Geocoder.google({
apiKey: GOOGLE.googleMapApiKey,
}),
}).on('routesfound', (e) => {
const routes = e.routes;
cartographyContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
const summary: RouteSummary = {
ascent: routes[0].routeSummary.ascent,
descent: routes[0].routeSummary.descent,
distance: routes[0].routeSummary.distance,
estimatedDuration: routes[0].routeSummary.duration
}
cartographyContext.setRouteSummary(summary);
})
setInstance(instance);
instance.addTo(map);
}, []);
useMapEvents({
click: (e: L.LeafletMouseEvent) => {
if (instance) {
if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else {
instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
}
}
}
});
return null;
};
export default RoutingMachine;

UseReducer hook doesn't update state (Can't perform a React state update on an unmounted component)

I'm trying to use useReducer instead of useState in a custom hook that loads the initial data from the API, and getting an error updating a state. (I use useReducer here for learning purposes).
The component fetches data firstly correctly, the error occurs when I update the state (book/edit/delete interview).
I left the previous useState code in the comments for better understanding.
import { useReducer, useEffect } from "react";
import axios from "axios";
const SET_DAY = "SET_DAY";
const SET_APPLICATION_DATA = "SET_APPLICATION_DATA";
const SET_INTERVIEW = "SET_INTERVIEW";
const reducer = (state, action) => {
switch (action.type) {
case SET_DAY:
return { ...state, day: action.day }
case SET_APPLICATION_DATA:
return {
...state,
days: action.days,
appointments: action.appointments,
interviewers: action.interviewers
}
case SET_INTERVIEW: {
return { ...state, id: action.id, interview: action.interview }
}
default:
throw new Error();
}
}
export default function useApplicationData() {
// const [state, setState] = useState({
// day: "Monday",
// days: [],
// appointments: {},
// interviewers: {}
// });
const initialState = {
day: "Monday",
days: [],
appointments: {},
interviewers: {}
};
const [state, dispatch] = useReducer(reducer, initialState);
//updates the spots remaining when book/edit/cancel interview
const updateSpots = (requestType) => {
const days = state.days.map(day => {
if(day.name === state.day) {
if (requestType === 'bookInterview') {
// return { ...day, spots: day.spots - 1 }
return dispatch({ type: SET_DAY, spots: day.spots - 1 });
}else {
// return { ...day, spots: day.spots + 1 }
return dispatch({ type: SET_DAY, spots: day.spots + 1 });
}
}
// return { ...day };
return dispatch({ type: SET_DAY, spots: day.spots });
});
return days;
}
//sets the current day data
// const setDay = day => setState(prev => ({ ...prev, day }));
const setDay = (day) => dispatch({ type: SET_DAY, day });
//adds new interview data to database
const bookInterview = (id, interview) => {
const appointment = { ...state.appointments[id] };
const bookOrEdit = appointment.interview ? 'edit' : 'book'; //defines the request type
appointment.interview = { ...interview };
const appointments = { ...state.appointments, [id]: appointment };
let days = state.days;
if (bookOrEdit === 'book') {
days = updateSpots('bookInterview');
}
return axios
.put(`/api/appointments/${id}`, {interview})
.then(() => {
//setState({ ...state, appointments, days });
dispatch({ type: SET_INTERVIEW, id, interview });
})
};
//deletes interview data from database
const cancelInterview = (id) => {
const appointment = {...state.appointments[id], interview: null};
const appointments = {...state.appointments, [id]: appointment };
const days = updateSpots();
return axios
.delete(`/api/appointments/${id}`)
.then(() => {
//setState({ ...state, appointments, days });
dispatch({ type: SET_INTERVIEW, id, interview: null });
})
};
useEffect(() => {
let isMounted = false;
Promise.all([
axios.get('/api/days'),
axios.get('/api/appointments'),
axios.get('/api/interviewers')
])
.then((all) => {
// setState(prev => ({
// ...prev,
// days: all[0].data,
// appointments: all[1].data,
// interviewers: all[2].data}));
// });
if (!isMounted) {
console.log("done!");
}
isMounted = true;
dispatch({ type: SET_APPLICATION_DATA, days: all[0].data, appointments: all[1].data, interviewers:all[2].data });
});
}, []);
return { state, setDay, bookInterview, cancelInterview }
};
I'd be appreciated for pointing me in the right direction on what I'm doing wrong. Thank you!

How to change the router history in a functional component using react-redux

I'm trying to migrate a code base from a class component to a functional component, but when I do that my history (browserHistory) and the redirects stop working.
I don't know if it's in the way I'm using useEffect, or forcing the history.push ? But I get a Warning: You cannot change <Router history> on Router and Connected Router. I get the right values from the state and redux and I can see the changes on the location when I console.log it is just that history.push that doesn't happen.
I'm using react-redux and connected-react-router.
What might the problem be?
Use case: Facebook API loads, when user signs in with Facebook, the user is redirected to another page.
This code works: class Component (old code)
class App extends Component {
constructor(props) {
super(props);
this.facebookInterval = undefined;
this.history = createBrowserHistory();
this.state = {
hasFB: false,
error: false,
};
}
/**
* Load the Facebook SDK
*
* #return {void}
*/
componentDidMount() {
this.facebookInterval = setInterval(() => {
if (window.FB !== undefined) {
this.setState({
hasFB: true,
});
clearInterval(this.facebookInterval);
}
}, 100);
window.fbAsyncInit = function () {
window.FB.init({
appId: process.env.REACT_APP_NEW_MESSENGER_APP_ID,
autoLogAppEvents: true,
xfbml: true,
version: 'v3.3',
});
};
(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {
return;
}
js = d.createElement(s);
js.id = id;
js.src =
'https://connect.facebook.net/en_US/sdk.js?xfbml=1&version=v9.0&appId=' +
process.env.REACT_APP_NEW_MESSENGER_APP_ID;
fjs.parentNode.insertBefore(js, fjs);
})(document, 'script', 'facebook-jssdk');
}
// [NOTE]: This works when using the class component
// the redirect happens
componentWillUpdate = (nextProps, nextState) => {
console.log('nextProps.redirect', nextProps.redirect)
console.log('nextProps', nextProps)
console.log('nextState', nextState)
if (nextProps.redirect !== undefined && nextProps.redirect !== '/') {
this.history.push(nextProps.redirect);
}
};
render() {
const { t } = this.props;
if (this.state.hasFB) {
return (
<>
<Elements stripe={stripePromise}>
<ConnectedRouter history={this.history}>{routes}</ConnectedRouter>
</Elements>
</>
);
} else {
return null;
}
}
}
const mapStateToProps = (state) => {
let isLoading = state.siteR.isLoading ? 'open' : 'close';
let data = {
redirect: state.siteR.location,
isLoading: isLoading,
user: state.userR[state.userR.id],
id: state.userR.id,
};
if (state.userR[state.userR.id] !== undefined) {
data.models = state.userR[state.userR.id].models;
}
return data;
};
const mapDispatchToProps = (dispatch) => {
return {
// ommited_code
};
};
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withTranslation(),
)(App);
Same code, but on a functional component (new code)
const App = (props) => {
console.log('props', props)
// console.log('props.redirect', props.redirect)
const [hasFB, setHasFB] = useState(false)
const [error, setError] = useState(false)
const [redirect, setRedirect] = useState(props.redirect)
const history = createBrowserHistory()
let facebookInterval = undefined
const {t} = useTranslation()
const initializeFacebook = () => {
facebookInterval = setInterval(() => {
if (window.FB !== undefined) {
setHasFB(true)
clearInterval(facebookInterval);
}
}, 100);
window.fbAsyncInit = function () {
window.FB.init({
appId: process.env.REACT_APP_NEW_MESSENGER_APP_ID,
autoLogAppEvents: true,
xfbml: true,
version: 'v3.3',
});
};
(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {
return;
}
js = d.createElement(s);
js.id = id;
js.src =
'https://connect.facebook.net/en_US/sdk.js?xfbml=1&version=v9.0&appId=' +
process.env.REACT_APP_NEW_MESSENGER_APP_ID;
fjs.parentNode.insertBefore(js, fjs);
})(document, 'script', 'facebook-jssdk');
}
// [NOTE]: I get the right values from props.redirect
// and the state
// the redirect just doesnt happen
const handleRedirect = () => {
if (props.redirect !== undefined && props.redirect !== '/') {
console.log('redirect on setRedirect', redirect)
setRedirect(history.push(props.redirect))
}
}
useEffect(() => {
initializeFacebook()
handleRedirect()
console.log('redirect on setRedirect', redirect)
}, [])
if (hasFB) {
return (
<>
<Elements stripe={stripePromise}>
<ConnectedRouter history={history}>{routes}</ConnectedRouter>
</Elements>
</>
);
} else {
return null;
}
}
const mapStateToProps = (state) => {
let isLoading = state.siteR.isLoading ? 'open' : 'close';
let showModelSelect = state.modelR.showModelSelect ? 'open' : 'close';
let showModelAdd = state.modelR.showModelAdd ? 'open' : 'close';
let data = {
redirect: state.siteR.location,
isLoading: isLoading,
user: state.userR[state.userR.id],
id: state.userR.id,
};
if (state.userR[state.userR.id] !== undefined) {
data.models = state.userR[state.userR.id].models;
}
return data;
};
const mapDispatchToProps = (dispatch) => {
return {
// ommited_code
};
};
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withTranslation(),
)(App);
The routes I'm trying to switch to/from
const routes = (
<div>
<Switch>
<SentryRoute exact path='/' component={UserLogin} />
<SentryRoute exact path='/user' component={Profile} />
{/* // omited_code */}
</Switch>
</div>
)
export default routes;
From the documentation, it looks like you might be able to use the additional params in the connect function.
import { push } from 'connected-react-router';
/* code stuffs */
props.push('/home');
export default connect(null, { push })(Component);
https://github.com/supasate/connected-react-router/blob/master/FAQ.md#with-react-redux

override one value with a new value gives value undefined

To summarize what I want to do:
Update the state depending on the previous state
I have searched in vain for a solution to the above problems. Found 3 solutions, unfortunately without any success.
1)
const Form = (props) => {
const [newValue, setNewValue] = useState(0);
const submitHandler = (e) => {
e.preventDefault();
const incrementOne = {
value: setNewValue((prevState) => {
return {...prevState, newValue: newValue + 1}
})
};
console.log(incrementOne);
};
const submitHandler = (e) => {
e.preventDefault();
const incrementOne = {
value: setNewValue(newValue + 1),
};
console.log(incrementOne);
};
3
const submitHandler = (e) => {
e.preventDefault();
const incrementOne = {
value: setNewValue(prevState => prevState + 1),
};
console.log(incrementOne);
};
Thank you in advance for your time and effort
Sincerely
/ Peter
In all your examples you are creating an object with a value property. You assume that is supposed to get it's value from calling set function returned by useState. However, the result of calling this function is updating the state, and re-rendering. The function itself doesn't return anything (undefined).
const incrementOne = {
value: setNewValue((prevState) => {
return {...prevState, newValue: newValue + 1}
})
};
You should call the setNewValue function when you want to update the value. You can calculate the new state using the previous one:
setNewValue(newValue + 1);
Or use a functional update to avoid depending on the state directly:
setNewValue(prevState => prevState + 1);
Note that the new value is only available after the component re-renders.
Example:
const { useState } = React;
const Form = (props) => {
const [newValue, setNewValue] = useState(0);
const submitHandler = () => {
setNewValue(prevState => prevState + 1);
};
const incrementOne = {
value: newValue,
};
console.log(incrementOne);
return (
<div>
<div>{newValue}</div>
<button onClick={submitHandler}>Submit</button>
</div>
);
}
ReactDOM.render(
<Form />,
root
)
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>

Mock api in react redux-thunk project returning undefined

I am quite new in redux world and have not yet had a project structured the ducks way. I am trying to understand it and use it to make a mock api, since I don't have the backend ready yet. I am working with the legacy code, that I am trying to figure out. There is a folder called data, that has a duck and a backendApi file. Duck file looks like this.
data/duck.jsx
import { createSelector } from 'reselect';
import { createReduxApi } from './backendApi';
const getDataContext = state => state.default.dataContext;
const backendReduxApi = createBackendReduxApi(getDataContext);
// Action creators
export const makeRestApiRequest = endpointName => backendReduxApi .makeRequestActionCreator(endpointName);
export const resetRestApi = endpointName => backendReduxApi .makeResetActionCreator(endpointName);
// Reducers
export const dataReducer = backendReduxApi .createReducer();
// Selectors
const getRestApiState = endpointName => backendReduxApi .getEndpointState(endpointName);
export const getRestApiData = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.data);
export const getRestApiMeta = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.meta);
export const getRestApiError = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.error);
export const getRestApiStarted = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.started);
export const getRestApiFinished = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.finished);
The backendApi.jsx file looks like this:
data/backendApi.jsx
import ReduxRestApi from './rest/ReduxRestApi';
export const BackendApi = { // NOSONAR
LANGUAGE_FILE: 'languageFile',
EMPLOYEE: 'employee',
};
const backendReduxApiBuilder = ReduxRestApi.build()
/* /api */
/* /api/employee */
.withGet('/myproject/api/employee', BackendApi.EMPLOYEE)
/* /language*/
.withGet('/myproject/language/nb_NO.json', BackendApi.LANGUAGE_FILE)
export const createBackendReduxApi = restApiSelector => backendReduxApiBuilder
.withRestApiSelector(restApiSelector)
.create();
Then in the data/rest folder I have 4 files: ReduxRestApi, restConfig, RestDuck and restMethods.
data/rest/ReduxRestApi.jsx
import { combineReducers } from 'redux';
import { get, post, postAndOpenBlob } from './restMethods';
import RestDuck from './RestDuck';
class ReduxRestApi {
constructor(endpoints, getRestApiState) {
this.createReducer = this.createReducer.bind(this);
this.getEndpoint = this.getEndpoint.bind(this);
this.makeRequestActionCreator = this.makeRequestActionCreator.bind(this);
this.makeResetActionCreator = this.makeResetActionCreator.bind(this);
this.getEndpointState = this.getEndpointState.bind(this);
this.ducks = endpoints.map(({ name, path, restMethod }) => new RestDuck(name, path, restMethod, getRestApiState));
}
createReducer() {
const reducers = this.ducks
.map(duck => ({ [duck.name]: duck.reducer }))
.reduce((a, b) => ({ ...a, ...b }), {});
return combineReducers(reducers);
}
getEndpoint(endpointName) {
return this.ducks.find(duck => duck.name === endpointName)
|| { actionCreators: {} };
}
makeRequestActionCreator(endpointName) {
return this.getEndpoint(endpointName).actionCreators.execRequest;
}
makeResetActionCreator(endpointName) {
return this.getEndpoint(endpointName).actionCreators.reset;
}
getEndpointState(endpointName) {
return this.getEndpoint(endpointName).stateSelector;
}
static build() {
class RestApiBuilder {
constructor() {
this.withGet = this.withGet.bind(this);
this.withPost = this.withPost.bind(this);
this.withPostAndOpenBlob = this.withPostAndOpenBlob.bind(this);
this.withRestApiSelector = this.withRestApiSelector.bind(this);
this.endpoints = [];
}
withGet(path, name) {
this.endpoints.push({ path, name, restMethod: get });
return this;
}
withPost(path, name) {
this.endpoints.push({ path, name, restMethod: post });
return this;
}
withPostAndOpenBlob(path, name) {
this.endpoints.push({ path, name, restMethod: postAndOpenBlob });
return this;
}
withRestApiSelector(restApiSelector) {
this.restApiSelector = restApiSelector;
return this;
}
create() {
return new ReduxRestApi(
this.endpoints,
this.restApiSelector
);
}
}
return new RestApiBuilder();
}
}
export default ReduxRestApi;
restConfig.jsx
import axios from 'axios';
import { removeErrorMessage, showErrorMessage } from '../../app/duck';
import { is401Error, isHandledError } from '../../app/ErrorTypes';
const isDevelopment = process.env.NODE_ENV === 'development';
const configureRequestInterceptors = (store) => {
const onRequestAccepted = (config) => {
store.dispatch(removeErrorMessage());
return config;
};
const onRequestRejected = error => Promise.reject(error);
axios.interceptors.request.use(onRequestAccepted, onRequestRejected);
};
const configureResponseInterceptors = (store) => {
const onSuccessResponse = response => response;
const onErrorResponse = (error) => {
if (is401Error(error) && !isDevelopment) {
window.location.reload();
}
if (!isHandledError(error)) {
store.dispatch(showErrorMessage(error));
}
return Promise.reject(error);
};
axios.interceptors.response.use(onSuccessResponse, onErrorResponse);
};
const configureRestInterceptors = (store) => {
configureRequestInterceptors(store);
configureResponseInterceptors(store);
};
export default configureRestInterceptors;
data/rest/RestDuck.jsx
import { createSelector } from 'reselect';
import { get, getBlob, post, postAndOpenBlob, postBlob } from './restMethods';
/**
* getMethodName
* Helper function that maps given AJAX-method to a name
*
* Ex. getMethodName(getBlob) -> 'GET'
*/
const getMethodName = (restMethod) => {
switch (restMethod) {
case get:
case getBlob:
return 'GET';
case post:
case postBlob:
case postAndOpenBlob:
return 'POST';
default:
return '';
}
};
/**
* createRequestActionType
* Helper function to generate actionType for actions related to AJAX calls
*
* Ex: createRequestActionType('fetchEmployee', 'ERROR', get, '/myproject/api/employee') -> '##REST/fetchEmployee GET /myproject/api/employeeERROR'
*/
const createRequestActionType = (name, qualifier, restMethod = '', path = '') => [`##REST/${name}`, getMethodName(restMethod), path, qualifier]
.filter(s => s !== '')
.join(' ');
/**
* createRequestActionTypes
* Helper function to generate ActionTypes for a given AJAX method and resource.
*
* Ex. createRequestActionType(fetchEmployee, get, '/myproject/api/employee') -> {
* reset: '##REST GET /myproject/api/employee RESET',
* requestStarted: '##REST GET /myproject/api/employee STARTED',
* requestError: '##REST GET /myproject/api/employee ERROR',
* requestFinished: '##REST GET /myproject/api/employee FINISHED',
* }
*/
const createRequestActionTypes = (name, restMethod, path) => ({
reset: createRequestActionType(name, 'RESET'),
requestStarted: createRequestActionType(name, 'STARTED', restMethod, path),
requestError: createRequestActionType(name, 'ERROR', restMethod, path),
requestFinished: createRequestActionType(name, 'FINISHED', restMethod, path)
});
/**
* createRequestThunk
* Helper function that generates a thunk that performs an AJAX call specified by 'restMethod' and 'restEndpoint'
*
* When the thunk is running, the action 'requestStarted' will be dispatched immediately.
* Then, it performs the AJAX call that returns a promise.
* If the call goes well, the action 'requestFinished' will be dispatched with data from the call.
* If the call fails, the action 'requestError' is dispatched with the contents of the error.
*/
const createRequestThunk = (restMethod, restEndpoint, requestStarted, requestFinished, requestError) => (
(params, options = {}) => (dispatch) => {
dispatch(requestStarted(params, options));
return restMethod(restEndpoint, params)
.catch((error) => {
const data = error.response && error.response.data ? error.response.data : error;
dispatch(requestError(data));
return Promise.reject(error);
})
.then((response) => {
dispatch(requestFinished(response.data));
return response;
});
}
);
/**
* createRequestActionCreators
* Helper function that creates action creators 'requestStarted', 'requestFinished' and 'requestError',
* #see createRequestThunkCreator
*/
const createRequestActionCreators = (restMethod, restEndpoint, actionTypes) => {
const reset = () => ({ type: actionTypes.reset });
const requestStarted = (params, options = {}) => ({ type: actionTypes.requestStarted, payload: { params, timestamp: Date.now() }, meta: { options } });
const requestFinished = data => ({ type: actionTypes.requestFinished, payload: data });
const requestError = error => ({ type: actionTypes.requestError, payload: error });
const execRequest = createRequestThunk(restMethod, restEndpoint, requestStarted, requestFinished, requestError);
return {
reset, requestStarted, requestFinished, requestError, execRequest
};
};
/**
* createRequestReducer
*
* Helper function that creates a reducer for an AJAX call.
* Reducer alters the state of the actions with the name defined by
* actionTypes.requestStarted
* actionTypes.requestFinished
* actionTypes.requestError
*/
const createRequestReducer = (restMethod, resourceName, actionTypes) => {
const initialState = {
data: undefined,
meta: undefined,
error: undefined,
started: false,
finished: false
};
return (state = initialState, action = {}) => {
switch (action.type) {
case actionTypes.requestStarted:
return {
...initialState,
data: action.meta.options.keepData ? state.data : initialState.data,
started: true,
meta: action.payload
};
case actionTypes.requestFinished:
return {
...state,
started: false,
finished: true,
data: action.payload
};
case actionTypes.requestError:
return {
...state,
started: false,
error: action.payload
};
case actionTypes.reset:
return {
...initialState
};
default:
return state;
}
};
};
/**
* RestDuck
* Class that offers action types, action creators, reducers and selectors for an AJAX call.
* #see createRequestActionTypes
* #see createRequestActionCreators
* #see createRequestReducer
*
* Ex.
* const getEmployeeDuck = new RestDuck(execGetRequest, 'employee', GET_EMPLOYEE_SERVER_URL);
* // Action creators
* export const fetchEmployee = getEmployeeDuck.actionCreators.execRequest;
* // Reducer
* export const dataReducer = combineReducers(
* ...,
* getEmployeeDuck.reducer,
* }
* // Selectors
* export const getDataContext = state => state.default.dataContext;
* export const getEmployeeData = getEmployeeDuck.selectors.getRequestData(getDataContext);
* export const getEmployeeStarted = getEmployeeDuck.selectors.getRequestStarted(getDataContext);
* ...
*/
class RestDuck {
constructor(name, path, restMethod, getApiContext) {
this.restMethod = restMethod;
this.name = name;
this.path = path;
this.getApiContext = getApiContext;
this.$$duck = {}; // for class internal use
}
get actionTypes() {
if (!this.$$duck.actionTypes) {
this.$$duck.actionTypes = createRequestActionTypes(this.name, this.restMethod, this.path);
}
return this.$$duck.actionTypes;
}
get actionCreators() {
if (!this.$$duck.actionCreators) {
this.$$duck.actionCreators = createRequestActionCreators(this.restMethod, this.path, this.actionTypes);
}
return this.$$duck.actionCreators;
}
get reducer() {
if (!this.$$duck.reducer) {
this.$$duck.reducer = createRequestReducer(this.restMethod, this.name, this.actionTypes);
}
return this.$$duck.reducer;
}
get stateSelector() {
return createSelector([this.getApiContext], restApiContext => restApiContext[this.name]);
}
}
export default RestDuck;
data/rest/restMethods.jsx
import axios, { CancelToken } from 'axios';
const openPreview = (data) => {
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(data);
} else {
window.open(URL.createObjectURL(data));
}
};
const cancellable = (config) => {
let cancel;
const request = axios({
...config,
cancelToken: new CancelToken((c) => { cancel = c; })
});
request.cancel = cancel;
return request.catch(error => (axios.isCancel(error) ? Promise.reject(new Error(null)) : Promise.reject(error)));
};
const defaultHeaders = {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: 0
};
const defaultPostHeaders = {
'Content-Type': 'application/json'
};
export const get = (url, params, responseType = 'json') => cancellable({
url,
params,
responseType,
method: 'get',
headers: {
...defaultHeaders
}
});
export const post = (url, data, responseType = 'json') => cancellable({
url,
responseType,
data: JSON.stringify(data),
method: 'post',
headers: {
...defaultHeaders,
...defaultPostHeaders
},
cache: false
});
export const getBlob = (url, params) => get(url, params, 'blob');
export const postBlob = (url, data) => post(url, data, 'blob');
export const postAndOpenBlob = (url, data) => postBlob(url, data)
.then((response) => {
openPreview(response.data);
return {
...response,
data: 'blob opened as preview' // Don't waste memory by storing blob in state
};
});
I am not sure where to place and how to do mock api calls in this structure. I was thinking of making a mock api similiar to this one, where I would mimick the ajax calls and store them in the redux, but just not sure how to do this in this kind of setup?
I have tried with making the mockApi folder and instead of using the restMethods, to use the file where I would write promises that would resolve the mockData. This is my attempt:
mockRestMethods
const employee = {
name: 'Joe Doe'
}
const data = {
employee
};
export const get = item => new Promise((resolve) => {
setTimeout(() => {
resolve({ data: data[item] });
}, 1000);
});
But, if I inspect what is returned as the response.data inside the createRequestThunk function in the RestDuck file I get data: undefined there. Why is that, what am I doing wrong?
I may well have this wrong, but it seems like you are replacing
export const get = (url, params, responseType = 'json') => cancellable({
with export const get = item => new Promise((resolve) => { which has a different API.
Regardless, have you tried logging the value of item in the mock get function. I'm guessing it isn't "employee" which is the only property in data.
Yes, that was my goal, to replace the call that was pointing to the backend API, with the call where I would return the mock data. I have tried to log the value of the item, but I get undefined
ok, so there's an awful lot of abstraction going on there. Id suggest starting by replacing get in data/rest/restMethods.jsx directly with a version that returns a promise, get it working, and then break it out. That way you're not dealing with too many unknowns at once.
I had done similar using redux-saga. After debugging, I had found that there must be data property as root key. Here's how you should do:
const employee = {
data: { // root key of employee
items: [
{ name: 'Bhojendra' },
{ name: 'Rauniyar' }
]
}
}
// and no need to use setTimeout, we're just resolving some constant data
export const getItems = item => new Promise(resolve => resolve(employee))
Now, I hope you know why data is undefined with your code.
Still not clear?
Response looks for the data property. That's it.

Resources