I load an essentially static configuration from the server using RTK Query which is parsed into a Context and used inside a Provider. I can happily use the configuration in elements with a useConfiguration() hook.
However, I want to use the configuration when processing actions.
Is it possible to get the content of the Context in a reducer?
Of course, I can pass the entire configuration as part of the action payload, but would like to avoid doing this. The reason is that the configuration is large (making the serialised actions large?), and it makes it more difficult to create actions in Storybook stories.
Here is the context:
export const ConfigurationProvider: FC = ({ children }) => {
const { data } = api.useGetConfigurationQuery()
const [state, dispatch] = useReducer(configurationReducer, initialState)
const value = useMemo(
() => ({ configuration: state, dispatch }),
[state, data]
)
useEffect(() => {
if (data) {
dispatch({ type: 'parse', payload: data })
}
}, [data])
return (
<ConfigurationContext.Provider value={value}>
{children}
</ConfigurationContext.Provider>
)
}
Here is where I want to use the configuration:
const mySlice = createSlice({
name: 'mySlice',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(quoteAction.addItem, (state, action) => {
// How can I get the configuration here.
})
},
})
Related
I want to process the data that I get from the request in the slice.
Because not all slices are async (but work with the same data), transformResponse is not suitable.
Is there anything you can suggest?
My code example:
Some RTK Query
export const currencyApi = createApi({
reducerPath: 'currencyApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.apilayer.com/exchangerates_data' }),
endpoints: (build) => ({
fetchCurrencyRates: build.query<IApiResponse, string>({
query: (currency) => ({
url: '/latest',
params: {
base: currency
},
headers: {
apikey: *SomeApiKey*
}
})
})
})
})
Slice where I want to use data from RTK requests
const initialState: ICurrencyState = {
currencyRates: {},
availableCurrencyOptions: [],
fromCurrency: '',
toCurrency: '',
exchangeRate: 0,
error: null
}
export const currencySlice = createSlice({
name: 'currency',
initialState,
reducers: {
//
}
})
Use Hooks in Components
You can send the received data to the slice via useEffect. Something like this:
const { data } = useFetchCurrencyRatesQuery();
useEffect(() => {
if (data !== undefined) {
dispatch(...)
}
}, [data])
I am new to Redux RTK so the problem might not exactly be on calling getSelectors(). However, when I'm using the state that comes from getSelectors() it reloads the entire state.
Problem
The baseline is that I have different Setup objects that I'm calling based on the documentId. These Setup objects are quite large so in the getSetups I am only fetching some basic properties. Then, when the user selects a specific Setup from the dropdown I want to save it in the setupSlice. But when I trigger the dispatch(setSetup(data)) the RTK reloads all the Setups.
I encounter an infinite loop when after fetching all the Setup objects I want to automatically assign the default Setup to the setupSlice.
Extra
Ideally when I assign a Setup to the setupSlice I would like to call the getSetup from RTK to fetch the entire Setup object of that specific Setup and store it in the setupSlice.
I am not sure if this is suppose to be happening but is there anyway to stop it? Otherwise is there any recommendation so I can move forward?
This is the component I'm trying to generate:
const SetupDropdown = () => {
const dispatch = useDispatch()
const { documentId } = useParams()
const { data, isFetching } = useGetSetupsQuery({ documentId })
let setupsMenu;
const { selectAll: selectAllSetups } = getSelectors({documentId})
const allSetups = useSelector(selectAllSetups)
if (!isFetching) {
const defaultSetup = allSetups.find((setup) => setup.default)
setupsMenu = allSetups.map(setup => {
return (<MenuItem value={setup.id}>{setup.name}</MenuItem>)
})
dispatch(setSetup(defaultSetup))
}
const setupId = useSelector(selectSetupId)
const handleChange = async (event) => {
// Here I ideally call the getSetup RTK Query to fetch the entire information of the single setup
const data = {
id: event.target.value,
name: 'Random name'
}
dispatch(setSetup(data))
};
return (
<FormControl sx={{ minWidth: 200 }} size="small">
<InputLabel>Setup</InputLabel>
<Select
value={setupId}
onChange={handleChange}
label="Setup"
>
{setupsMenu}
</Select>
</FormControl>
)
}
export default SetupDropdown;
This is the setupApiSlice:
const setupsAdapter = createEntityAdapter({
sortComparer: (a, b) => b.date.localeCompare(a.date)
})
const initialState = setupsAdapter.getInitialState()
export const setupsApiSlice = apiSlice.injectEndpoints({
tagTypes: ['Setup'],
endpoints: builder => ({
getSetups: builder.query({
query: ({ documentId }) => ({
url: `/documents/${documentId}/setups`,
method: 'GET'
}),
transformResponse: responseData => {
return setupsAdapter.setAll(initialState, responseData)
},
providesTags: (result, error, arg) => [
{ type: 'Setup', id: "LIST" },
...result.ids.map(id => ({ type: 'Setup', id }))
]
}),
getSetup: builder.query({
query: ({ documentId, setupId }) => ({
url: `/documents/${documentId}/setups/${setupId}`,
method: 'GET'
})
})
})
})
export const {
useGetSetupsQuery,
useGetSetupQuery
} = setupsApiSlice
// Define function to get selectors based on arguments (query) of getSetups
export const getSelectors = (
query,
) => {
const selectSetupsResult = setupsApiSlice.endpoints.getSetups.select(query)
const adapterSelectors = createSelector(
selectSetupsResult,
(result) => setupsAdapter.getSelectors(() => result?.data ?? initialState)
)
return {
selectAll: createSelector(adapterSelectors, (s) =>
s.selectAll(undefined)
),
selectEntities: createSelector(adapterSelectors, (s) =>
s.selectEntities(undefined)
),
selectIds: createSelector(adapterSelectors, (s) =>
s.selectIds(undefined)
),
selectTotal: createSelector(adapterSelectors, (s) =>
s.selectTotal(undefined)
),
selectById: (id) => createSelector(adapterSelectors, (s) =>
s.selectById(s, id)
),
}
}
This is the setupSplice:
const initialState = {
name: null,
filters: [],
data: {},
status: 'idle', //'idle' | 'loading' | 'succeeded' | 'failed'
error: null
}
const setupSlice = createSlice({
name: 'setup',
initialState,
reducers: {
setSetup: (state, action) => {
console.log('Dispatch')
const setup = action.payload;
console.log(setup)
state.id = setup.id;
state.name = setup.name;
state.filters = setup.filters;
state.data = setup.state;
state.status = 'succeeded';
}
}
})
export const { setSetup } = setupSlice.actions;
export const selectSetupId = (state) => state.setup.id;
export const selectSetupName = (state) => state.setup.name;
export const selectSetupFilters = (state) => state.setup.filters;
export const selectSetupData = (state) => state.setup.data;
export default setupSlice.reducer;
Tbh., you probably should be using selectFromResult in your useGetSetupsQuery instead of adding another useSelector hook. That would also reduce your code complexity by a lot.
Your problem as hand is that you are creating those selectors within your component on each render - so they don't have a chance to actually memoize and give you a stable result. If you do that in your component, wrap it in a useMemo call to keep your selector instances as stable as possible.
My react app uses a redux connected component to render data from backend for a project page, so I called a GET dispatch inside a React Hook useEffect to make sure data is always rendered when the project page first open, and whenever there is a change in state project, the component will be updated accordingly using connect redux function. However, the component doesn't update after I reduce the new state using a DELETE API request, only if I dispatch another GET request then the state will be updated. So I have to call 2 dispatches, one for DELETE and one for GET to get the page updated synchronously (as you can see in handleDeleteUpdate function), and the same thing happened when I dispatch a POST request to add an update (in handleProjectUpdate). Only when I reload the page, the newly changed data will show up otherwise it doesn't happen synchronously, anyone knows what's wrong with the state update in my code? and how can I fix this so the page can be loaded faster with only one request?
I've changed the reducer to make sure the state is not mutated and is updated correctly.
I have also tried using async function in handleDeleteUpdate to make sure the action dispatch is finished
I have tried
console.log(props.project.data.updates)
to print out the updates list after calling props.deleteUpdate but it seems the updates list in the state have never been changed, but when I reload the page, the new updates list is shown up
Here is the code I have for the main connected redux component, actions, and reducers file for the component
function Project(props) {
let options = {year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'}
const {projectID} = useParams();
const history = useHistory();
console.log(props.project.data? props.project.data.updates : null);
console.log(props.project.data);
// const [updates, setUpdates] = useState(props.project.data? props.project.data.updates : null)
useEffect(() => {
props.getProject(projectID);
}, []);
// Add an update to project is handled here
const handleProjectUpdate = async (updateInfo) => {
await props.postProjectUpdate(projectID, updateInfo)
await props.getProject(projectID);
}
const handleDeleteUpdate = async (updateID) => {
await props.deleteUpdate(projectID, updateID);
await props.getProject(projectID);
console.log(props.project.data.updates);
};
return (
<div>
<Navbar selected='projects'/>
<div className = "project-info-layout">
<UpdateCard
updates = {props.project.data.updates}
handleProjectUpdate = {handleProjectUpdate}
handleDeleteUpdate = {handleDeleteUpdate}
options = {options}
/>
</div>
</div>
)
}
const mapStateToProps = state => ({
project: state.project.project,
});
export default connect(
mapStateToProps,
{getProject, postProjectUpdate, deleteUpdate}
)(Project);
ACTION
import axios from 'axios';
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from './types';
let token = localStorage.getItem("token");
const config = {
headers: {
Authorization: `Token ${token}`,
}
};
export const getProject = (slug) => dispatch => {
axios.get(`${backend}/api/projects/` + slug, config)
.then(
res => {
dispatch({
type: GET_PROJECT_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const postProjectUpdate = (slug, updateData) => dispatch => {
axios.post(`${backend}/api/projects/`+slug+ `/updates`,updateData, config)
.then(
res => {
dispatch({
type: ADD_PROJECT_UPDATE_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const deleteUpdate = (slug, updateID) => dispatch => {
axios.delete(`${backend}/api/projects/`+ slug + `/updates/`+ updateID, config)
.then(
res => {
dispatch({
type: DELETE_PROJECT_UPDATE_SUCCESS,
payload: updateID,
});
},
).catch(err => console.log(err));
}
Reducer
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from "../actions/types";
const initialState = {
project: {},
};
export default function ProjectReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_PROJECT_SUCCESS:
return {
...state, // return all initial state
project: payload
};
case ADD_PROJECT_UPDATE_SUCCESS:
return {
...state,
project: {
...state.project,
updates: [...state.project.data.updates, payload.data]
}
};
case DELETE_PROJECT_UPDATE_SUCCESS:
let newUpdatesArray = [...state.project.updates]
newUpdatesArray.filter(update => update.uuid !== payload)
return {
...state,
project: {
...state.project,
members: newUpdatesArray
}
};
default:
return state;
}
}
updateCard in the Project component is showing a list of all updates
So I basically have a websocket connection, this allows me to send generic messages via WEBSOCKET_MESSAGE_SEND and receive them via WEBSOCKET_MESSAGE_RECEIVED actions.
However there are cases where I want to make a request in a similar manner to a Ajax REST call. Eg to request a list of documents for a user I probably want to have an epic:
Receive an action eg ({ type: GET_DOCUMENTS })
Generate a random key to track the current request, we will call it 'request_id'
Send a ({ type: WEBSOCKET_MESSAGE_SEND, request_id }) action.
Wait for either of
an action ({ type: WEBSOCKET_MESSAGE_RECEIVED, request_id, message }) **Must be with a matching 'request_id' otherwise it should be ignored.
-> Emit an action eg ({ type: GET_DOCUMENTS_SUCCESS, documents: message })
a timeout eg 10 seconds
-> Emit an action eg ({ type: GET_DOCUMENTS_TIMEOUT })
I have been struggling to put this into code, I think the most awkward part of the whole epic is that I want to emit an action in the middle of my epic and wait. This doesn't feel quite right to me... ani-pattern? But I am not really sure how I should be doing this.
That's right. There is no good way to emit an action in the middle of an epic. How about splitting the epic into two?
const getDocumentsEpic = action$ =>
action$.pipe(
ofType("GET_DOCUMENTS"),
map(() => {
const requestId = generateRequestId();
return {
type: "WEBSOCKET_MESSAGE_SEND",
requestId
};
})
);
const websocketMessageEpic = action$ =>
action$.pipe(
ofType("WEBSOCKET_MESSAGE_SEND"),
switchMap(requestId => {
return action$.pipe(
ofType("WEBSOCKET_MESSAGE_RECEIVED"),
filter(action => action.requestId === requestId),
timeout(10000),
map(({ message }) => ({
type: "GET_DOCUMENTS_SUCCESS",
documents: message
})),
catchError(() => of({ type: "GET_DOCUMENTS_TIMEOUT" }))
);
})
);
Updated answer (2020-04-17):
I was unhappy with my original answer so decided to give it another shot.
NotificationOperators.js
import { of } from 'rxjs';
import { map, switchMap, filter, timeout, catchError, first, mergeMap } from 'rxjs/operators';
import { notificationActionTypes } from '../actions';
const NOTIFICATION_TIMEOUT = 60 * 1000;
const generateRequestId = () => Math.random().toString(16).slice(2);
const toNotificationRequest = notificationRequest => input$ =>
input$.pipe(mergeMap(async action => ({
type: notificationActionTypes.WEBSOCKET_MESSAGE_SEND,
message: {
request_id: generateRequestId(),
...(
typeof notificationRequest === "function" ?
await Promise.resolve(notificationRequest(action)) :
({ eventType: notificationRequest })
)
}
})));
const mapNotificationRequestResponses = (notificationRequest, mapper) => $input =>
$input.pipe(
filter(action =>
action.type === notificationActionTypes.WEBSOCKET_MESSAGE_SEND &&
action.message.eventType === notificationRequest),
concatMap(sendAction =>
$input.pipe(
filter(receiveAction => {
return (
receiveAction.type === notificationActionTypes.WEBSOCKET_MESSAGE_RECEIVED &&
receiveAction.message.request_id === sendAction.message.request_id
)
}),
first(),
timeout(NOTIFICATION_TIMEOUT),
map(({ message }) => mapper(message.success ? false : message.error, message.result, sendAction.message)),
catchError(errorMessage => of(mapper(errorMessage && errorMessage.message, null, sendAction.message))))));
export { toNotificationRequest, mapNotificationRequestResponses };
Usage:
export const getDocumentsReqEpic = action$ => action$.pipe(
ofType(documentActionTypes.REFRESH_DOCUMENTS_REQUEST),
toNotificationRequest(EventTypes.get_user_documents_req)
);
export const getDocumentsRecEpic = action$ => action$.pipe(
mapNotificationRequestResponses(
EventTypes.get_user_documents_req,
(error, result) => error ? refreshDocumentsError(error) : refreshDocumentsSuccess(result))
);
Original answer:
As I felt I would likely need to repeat this process many more times, this seemed like a reasonable amount of duplicated boilterplate that I should create a method to generate epics based on requirements. For this reason I have expanded upon #sneas awesome answer and have posted below incase it helps others.
Note this implementation assumes the websocket implementation from the other answer. It also assumes that the server websocket implementation will accept a 'request_id' and respond with the same 'request_id' so that request and response messages can be linked. Probably also worth noting that the 'epicLinkId' is client-side only, and simply enables the 2 epics being created to be linked to each other, without this you would only be able to call createNotifyReqResEpics() once.
createNotifyReqResEpics.js (helper based on code above)
import { ofType } from 'redux-observable';
import { of } from 'rxjs';
import { map, switchMap, filter, timeout, catchError, first } from 'rxjs/operators';
import { notificationActionTypes } from '../actions';
const generateRequestId = () => Math.random().toString(16).slice(2);
export default ({
requestFilter,
requestMessageMapper,
responseMessageMapper
}) => {
if (typeof requestFilter !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'requestFilter' argument.");
if (typeof requestMessageMapper !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'requestMessageMapper' argument.");
if (typeof responseMessageMapper !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'responseMessageMapper' argument.");
const epicLinkId = generateRequestId();
const websocketSendEpic = action$ =>
action$.pipe(
filter(requestFilter),
map(action => ({
epic_link_id: epicLinkId,
type: notificationActionTypes.WEBSOCKET_MESSAGE_SEND,
message: {
request_id: generateRequestId(),
...requestMessageMapper(action)
}
}))
);
const websocketReceiveEpic = action$ =>
action$.pipe(
ofType(notificationActionTypes.WEBSOCKET_MESSAGE_SEND),
filter(action => action.epic_link_id === epicLinkId),
switchMap(sendAction =>
action$.pipe(
ofType(notificationActionTypes.WEBSOCKET_MESSAGE_RECEIVED),
filter(receiveAction => receiveAction.request_id === sendAction.request_id),
first(),
timeout(10000),
map(receiveAction => responseMessageMapper(false, receiveAction.message)),
catchError(errorMessage => of(responseMessageMapper(errorMessage && errorMessage.message, null))))));
return [websocketSendEpic, websocketReceiveEpic];
};
documents.js (epics)
import EventTypes from '../shared-dependencies/EventTypes';
import { documentActionTypes, refreshDocumentsError, refreshDocumentsSuccess } from '../actions';
import { createNotifyReqResEpics } from '../utils';
const [getDocumentsReqEpic, getDocumentsRespEpic] = createNotifyReqResEpics({
requestFilter: action => action.type === documentActionTypes.REFRESH_DOCUMENTS_REQUEST,
requestMessageMapper: action => ({ eventType: EventTypes.get_user_documents_req }),
responseMessageMapper: (error, action) => error ? refreshDocumentsError(error) : refreshDocumentsSuccess(action.result)
});
export { getDocumentsReqEpic, getDocumentsRespEpic };
Where the 2 exported epics from documents.js make thie way into combineEpics.
I would like to use useReducer from react-hooks and rxjs together.
For example, I would like to fetch data from an API.
This is the code I wrote in order to do that:
RXJS hook:
function useRx(createSink, data, defaultValue = null) {
const [source, sinkSubscription] = useMemo(() => {
const source = new Subject()
const sink = createSink(source.pipe(distinctUntilChanged()));
const sinkSubscription = sink.subscribe()
return [source, sinkSubscription]
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
source.next(data)
}, [source, data])
useEffect(() => {
return () => {
sinkSubscription.unsubscribe()
};
}, [sinkSubscription])
}
Reducer code:
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_LOADING':
return {
...state,
loading: true
};
case 'FETCH_SUCCESS':
return {
...state,
loading: false,
total: action.payload.total,
data: action.payload.data
};
case 'FETCH_FAILURE':
return {
...state,
error: action.payload
};
case 'PAGE':
return {
...state,
page: action.page,
rowsPerPage: action.rowsPerPage
};
default:
throw new Error();
}
};
How i mix them:
function usePaginationReducerEndpoint(callbackService) {
const defaultPagination = {
statuses: null,
page: 0,
rowsPerPage: 10,
data: [],
total: 0,
error: null,
loading: false
}
const [pagination, dispatch] = useReducer(dataFetchReducer, defaultPagination)
const memoPagination = useMemo(
() => ({
statuses: pagination.statuses,
page: pagination.page,
rowsPerPage: pagination.rowsPerPage
}),
[pagination.statuses, pagination.page, pagination.rowsPerPage]
);
useRx(
memoPagination$ =>
memoPagination$.pipe(
map(memoPagination => {
dispatch({type: "FETCH_LOADING"})
return memoPagination
}),
switchMap(memoPagination => callbackService(memoPagination.statuses, memoPagination.page, memoPagination.rowsPerPage).pipe(
map(dataPagination => {
dispatch({ type: "FETCH_SUCCESS", payload: dataPagination })
return dataPagination
}),
catchError(error => {
dispatch({ type: "FETCH_SUCCESS", payload: "error" })
return of(error)
})
))
),
memoPagination,
defaultPagination,
2000
);
function handleRowsPerPageChange(event) {
const newTotalPages = Math.trunc(pagination.total / event.target.value)
const newPage = Math.min(pagination.page, newTotalPages)
dispatch({
type: "PAGE",
page: newPage,
rowsPerPage: event.target.value
});
}
function handlePageChange(event, page) {
dispatch({
type: "PAGE",
page: page,
rowsPerPage: pagination.rowsPerPage
});
}
return [pagination, handlePageChange, handleRowsPerPageChange]
}
The code works but I'm wondering if this is luck or not...
Is it ok if I dispatch inside RXJS pipe ? What is the risk ?
If no, how can I mix both useReducer and RXJS ?
If it's not the good approach, what is the good one ?
I know this ressource: https://www.robinwieruch.de/react-hooks-fetch-data/. But I would like to mix the power of hooks and RXJS in order to use, for example, the debounce function with rxjs in an async request...
Thanks for your help,
All you need is a middleware to connect useReducer and rxjs rather than create one yourself.
Using useReducer will create a lot of potential hard to debug code and also need an independent container component to put useReducer to prevent accident global rerendering.
So I suggest that using redux places useReducer to create global state from a component and use redux-observable (RxJS 6-based middleware for Redux) as middleware to connect rxjs and redux.
if you know rxjs well, it will be very easy to use, as official web show, fetch data from api will be:
https://redux-observable.js.org/docs/basics/Epics.html
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response))
)
)
);