The situation
I have an onboarding scenario where the user goes through a step-by-step onboarding. I want to manage the client side state of the user's progress with Redux. The synchronization between the server and the client is already implemented in Relay, but I still need a Redux store for client-side state management. As such, problems arise with synchronizing the Relay-/Redux-Store.
What I'm doing right now is to wrap my React component with Redux and then with Relay:
// OnboardProgressView.js
// ...
// wrap React component with Redux
const mapStateToProps = (state) => {
return {
onboardProgress: state.onboardProgress,
}
}
const ReduxContainer = connect(
mapStateToProps,
)(OnboardProgressView)
// this is only for convenience of access of the Relay data
const MappedOnboardProgressView = mapProps({
params: (props) => props.params,
user: (props) => props.viewer.user,
})(ReduxContainer)
// wrap Redux component with Relay
export default Relay.createContainer(MappedGettingStartedView, {
fragments: {
viewer: () => Relay.QL`
fragment on Viewer {
user {
userId
onboardProgressStep
}
# more stuff ...
}
`,
},
})
My progress
I have found ways to accomplish different operations as follows:
Initialization of the Redux store with server data
I am initializing the Redux state right after creating the store with an asynchronous raw Relay query. To make that possible I am also using the redux-thunk middleware. Redux initiates a request to Relay which queries the server. Visual representation (an arrow denotes data flow, the order of elements reflects the 'call order'): Redux <= Relay <= Server
// app.js
const store = createStore(reducer, applyMiddleware(thunk))
store.dispatch(fetchOnboardProgress())
// onboardProgress.js
export function fetchOnboardProgress () {
return function (dispatch) {
var query = Relay.createQuery(Relay.QL`
query {
viewer {
user {
id
onboardProgress
}
}
}`, {})
return new Promise(function (resolve, reject) {
Relay.Store.primeCache({query}, ({done, error}) => {
if (done) {
const data = Relay.Store.readQuery(query)[0]
dispatch(update(data.user.onboardProgress, data.user.id))
resolve()
} else if (error) {
reject(Error('Error when fetching onboardProgress'))
}
})
})
}
}
Updating data on server when dispatching a Redux action
Redux => Relay => Server
To have consistent state changes, when the user progresses through the onboarding process, I fire a Redux action that will also asynchronously do a Relay mutation. I am also using redux-thunk for this purpose.
function nextStep () {
return function (dispatch, getState) {
const currentStep = getState().onboardProgress.step
const currentStepIndex = OnboardProgress.steps.indexOf(currentStep)
const nextStep = OnboardProgress.steps[currentStepIndex + 1]
const userId = getState().onboardProgress._userId
return _updateReduxAndRelay(dispatch, nextStep, userId)
}
}
function _updateReduxAndRelay (dispatch, step, userId) {
return new Promise((resolve, reject) => {
Relay.Store.commitUpdate(new UpdateUserMutation({
userId: userId,
onboardProgressStep: step,
}), {
onSuccess: () => {
dispatch(update(step, userId))
resolve()
},
onFailure: reject,
})
})
}
export function update (step, userId) {
const payload = {onboardProgress: new OnboardProgress({step, userId})}
return {type: UPDATE, payload}
}
Open Problems
I still haven't find an approach to the following situation:
Updating the Redux Store when the Relay Store updates
Changes to data on the server might have external sources, that are not triggered by a user action in our app. With Relay we can solve this with forceFetching or polling. A Relay query looks like this: Relay <= Server. I'd like to additionally have this data flow: Relay => Redux when external data changes.
Another possible reason for the need to update the Redux store with new data is when we want to synchronize data that is deeply nested in the Relay store, or part of a complex query.
For example, think of the count of comments to a blog post. When a user is posting a new comment, another component showing the comment count should update as well.
If we manage this information in Redux, we need a way to trigger a Redux action when a Relay query comes with new information. I am not aware of such a callback, or another solution to this situation.
My Questions
In this context, I have those questions:
What can I improve in my existing approaches? Is there something I did that is highly dangerous/leads to inconsistencies? (see My Progress)
How can I manage to sync the Redux store when for some reason the Relay store is being updated. I am looking for a React component life cycle method or a Relay callback where I can then send a Redux action to the Redux store. (see Open Problems)
RelayNetworkLayer is what you should use to sync the redux store with the relay one as it allows you to subscribe to everything that happens there. I'll update this post later if anything else comes to mind.
Related
I want to put the authenticated user in a zustand store. I get the authenticated user using react-query and that causes some problems. I'm not sure why I'm doing this. I want everything related to authentication can be accessed in a hook, so I thought zustand was a good choice.
This is the hook that fetches auth user:
const getAuthUser = async () => {
const { data } = await axios.get<AuthUserResponse>(`/auth/me`, {
withCredentials: true,
});
return data.user;
};
export const useAuthUserQuery = () => {
return useQuery("auth-user", getAuthUser);
};
And I want to put auth user in this store:
export const useAuthStore = create(() => ({
authUser: useAuthUserQuery(),
}));
This is the error that I get:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons.
you can read about it in the react documentation:
https://reactjs.org/warnings/invalid-hook-call-warning.html
(I changed the name of some functions in this post for the sake of understandability. useMeQuery = useAuthUserQuery)
I understand the error but I don't know how to fix it.
The misunderstanding here is that you don’t need to put data from react query into any other state management solution. React query is in itself a global state manager. You can just do:
const { data } = useAuthUserQuery()
in every component that needs the data. React query will automatically try to keep your data updated with background refetches. If you don’t need that for your resource, consider setting a staleTime.
—-
That being said, if you really want to put data from react-query into zustand, create a setter in zustand and call it in the onSuccess callback of the query:
useQuery(key, queryFn, { onSuccess: data => setToZustand(data) })
After some researches, I found some questions on stackoverflow about what I am trying to achieve, however, I don't feel that these questions and their answers gives me the "answers" or the "directions" i am looking for..
Note: I am pretty new to react even if I already made 2 projects and implemented redux into one of them. However, I ain't new at all in C# or in Go, even less in C. Based on my experience, I am just used to some architectures and I would like to reproduce one of them.
Here is a pretyy good schema from a similar question of mine:
Situation:
So let say I have pages that contains Components. I want these pages/compoments to display some stuff. One of my functionnality is to discover a map and for that, when the client moves, he gets new parts from my API. However, I don't wanna ask the server to give me the new parts and the ones I discovered already.
My idea about it would be to use a service MapService.js. This one would just store the discovered pieces of the map discovered and ask the server automatically about the new ones, and of course, store the new ones (concat).
However, I have to be logged for this, so I would like an ApiService.js that would store my authentication data and automatically put them in each of my requests.
Based on what I said, we would have something as:
Page -> Component -> Service -> API
From this, the API response would be gotten by my service, handled, then returned to the component. Handled means (data added to the previous then all returned)
I saw on internet one question that was referring "MVCS" (Model View Controller Service) pattern and I think I am looking for something as but I am not sure about how to implement it in ReactJs.
Redux seems to be something that you put all around and everywhere in your solution. What I would like is to use it as a "repository" let say, to be able to manage it from a service and not from the component itself. However, a service should be a single instance shared across the app and I don't know if something such as dependency injection could be the solution in ReactJS
Feel free to ask any edit if you need more details :)
Thanks for your help !
Here is a minimal example of Redux middleware usage. Usually, redux devs are using libraries (that give you a middleware) to have access to more appropriate APIs.
Redux middleware are chained, so each middleware can call the next middleware. The first middleware of the chain is called every time dispatch function (you can have it from react-redux connect) is called. In a middleware, if there is no next middleware it is the reducers that will be called. The next middleware can be call asynchronously after receiving an action. (Redux docs will still be better than my explainations).
In my example there is a catService that provide function that call rest API. Your services can be anything (a Class instance or a singleton for example). Usually in React/Redux stack, devs don't use object oriented development.
If a component dispatch getCat(123), the catMiddleware will be called (synchronously). Then requestGetCat will be called with the id 123. When the promise returned by requestGetCat will be resolved a setCat action will be send through the reducers to update the redux state. Once the redux state is done, the component listening for cats items object will be update too (triggering a rerender).
That can look very complexe, but in fact, it is very scalable and convenient.
// catService.js
// return a promise that return a cat object
const requestGetCat = id =>
fetch(`www.catcat.com/api/cat/${id}`)
.then(response => response.json())
// catTypes.js
export const GET_CAT = 'GET_CAT'
export const SET_CAT = 'SET_CAT'
// catActions.js
export const getCat = id => ({
type: GET_CAT,
id
})
export const setCat = (cat, id) => ({
type: SET_CAT,
id,
cat
})
// catReducer.js
const initialState = {
items: {}
}
const catReducer = (state = initialState, action) => {
if (action.type === SET_CAT) {
return {
items: {
...state.items,
[action.id]: action.cat
}
}
}
}
// catMiddleware.js
const handleGetCat = (next, action) => {
requestGetCat(action.id)
.then(cat => next(setCat(cat, action.id)))
// after retrieving the cat send an action to the reducers (or next middleware if it exist)
}
const actionHandlers = {
[GET_CAT]: handleGetCat
}
// receive every actions passing by redux (if not blocked)
// store: { dispatch, getState }
// next: next middleware or reducers (that set redux state)
// action: a redux action (dispatched) with at least type property
const catMiddleware = store => next => action => {
const handler = actionHandlers[action.type]
if (handler) {
handler(next, action)
} else {
// passing the action to the next middleware (or reducer - when there is no next middleware)
next(action)
}
}
// you have to apply your middleware
// and your reducer (see redux doc)
This one would just store the discovered pieces of the map discovered and ask the server automatically about the new ones, and of course, store the new ones
This is something I've wanted to do in the past, but never implemented a solution for.
The issue is that you essentially want to "cross the streams"..
In Redux there are two separate streams, ie dispatch an action to update the store, and read data from the store. Each of these are executed separately from a component. Combined, they can be used in a cycle by calling an action to load data into the store which triggers an update of the component which then reads from the store.
Basically you can't have non-component code that reads from the store, and if the data is missing, fires an action to load the data, then returns the data.
Thinking about it now, I'm wondering if the way to do this without adding logic to your view component is to wrap it in a component (HOC) that provides the logic.
The HOC will check the state for the location specified in the props. If it doesn't find it, it will dispatch an action to fetch it and render a loading display. When the state is updated with the new location it will update and render the wrapped component.
You could optionally always render the wrapped component and have it cope with the missing location until it is updated with the location set..
untested brain-dump below
loader HOC:
import React, { useEffect } from "react";
import actions from "./actions";
function withLocationLoader(Component) {
const Wrapper = function ({ location, locations, loadLocation, ...props }) {
useEffect(() => {
if (!locations[location]) {
loadLocation(location);
}
}, [locations]);
if (locations[location]) {
return <Component locations={locations} {...props} />;
}
return <div>Loading...</div>;
}
const mapStateToProps = (state, ownProps) => {
return { locations: state.locations };
};
const mapActionsToProps = {
loadLocation: actions.loadLocation,
};
return connect(
mapStateToProps,
mapActionsToProps
)(Wrapper);
}
export { withLoader };
component:
function MyBareComponent({ locations }) {
return <div>{JSON.stringify(locations)}</div>;
}
const MyComponent = withLocationLoader(MyBareComponent);
export { MyComponent };
actions: (utilising redux-thunk middleware)
function setLocation(location, data) {
return { type: "SET_LOCATION", payload: { location, data } };
}
export function loadLocation(location) {
return dispatch =>
Promise.resolve({ geoData: "" }) // mock api request
.then(data => dispatch(setLocation(location, data)));
}
I have started my react.js project using one of Starter kit. I have used one Boilerplate structure. I am trying to learn the actual flow of React
Redux Boilerplate. After gone through their structure I am able to call the API and got data on Component. Here is the flow what I have learned as per the structure:-
a. On componentDidMount I have called a method of Mudules to create an Action.
// On view
componentDidMount() {
this.props.getCampaignTopics(1);
}
//On Modules
export const getCampaignTopics = createAction(GET_CAMPAIGN_TOPICS, (id : string) => ({id}));
b. On saga when action has been fired, I am calling the API endpoint to get the data
// On Saga
export function* getCampaignTopics(actions) {
let id=0;
if(actions.payload.id)
{
id= actions.payload.id;
}
const responsetype = yield call(() => fetch(__CONFIG__.api_url+'topic/' + id,{method:'GET',data:{}}).then(response => response.json()))
yield put(addcampaignActions.setCampaignTopics(responsetype.data));
}
function* watchGetHome() {
yield takeLatest(addcampaignConstants.GET_CAMPAIGN_TOPICS, getCampaignTopics);
}
c. After Getting the data I am calling another method of Modules to set data
export const setCampaignTopics = createAction(SET_CAMPAIGN_TOPICS, (topics : any) => ({topics}));
export const reducers = {
[GET_CAMPAIGN_TOPICS]: (state, { payload }) =>
state.merge({
...payload,
}),
[SET_CAMPAIGN_TOPICS]: (state, { payload }) =>
state.merge({
...payload,
}),
}
d. In View I am using selector to separate the indexes
// Selector
const addcampaignDataSelector = state => state.addcampaign;
const topicSelector = createSelector(
addcampaignDataSelector,
payload => payload.get('topics')
);
export const addcampaignSelector = state => ({
topics: topicSelector(state)
});
e. In component I am getting the value on props
// render on Component
const { addcampaign } = this.props;
topics = addcampaign.topics.toJS().children;
So, that's the flow I have got from their tutorial and I am getting the data. But I want to make sure I am on right track or not? Please tell me this is the right process or not?
Next is I want to call a method from component and want receive the response on the component, so I can get the data on a local variable. Could anyone can give me some tutorial or example, So I can go through and learn easily.
If your looking for a production ready boilerplate,
react-boilerplate is one of the popular ones.
You seem to be on the right track. This diagram from react boilerplate guide might help in understanding the flow better.
I find myself in situations with React/Redux apps where I require a lot of functions to transform data when it comes back from the server by using libaries like moment or jwtDecode. For example:
function mapDispatchToProps(dispatch) {
return {
getAllTokens: () => {
dispatch(getAllTokens());
}
};
}
On page load, I run this.props.getAllTokens() to bring back all tokens from the server stored in the database which I'd like to show on the page (for example).
The dispatch uses Redux Thunk to perform some operations on the data and send to the store:
export const getAllTokens = () => dispatch => {
apiGetAllUsers()
.then(tokens => {
let newFormat = tokens.message.map(token => {
if (token.refreshToken) {
let decoded = jwtDecode(token.refreshToken);
let expiresIn = moment.unix(decoded.exp).fromNow();
return {
refreshToken: token.refreshToken,
email: token.email,
expiresIn
};
}
});
dispatch(adminTokensReceived(newFormat));
})
};
This function getAllTokens is in another file to help the component keep lean. This approach works fine for Redux, but when it comes to using Apollo instead, how would I update the data before adding it back to the component props?
I have came across this link which shows how to update data from a query, but I don't see many examples using it, so I wondered if I'm missing something fundamental?
So my questions is, what is the best way to use helper functions as I've described above?
Thanks
From the discussion here it seems that the state of Redux reducers should be persisted in a database.
How does something like user authentication works in this instance?
Wouldn't a new state object be created to replace the previous state in the database for every user (and their application state) created and edited?
Would using all of this data on the front end and constantly updating the state in the database be performant?
Edit: I've created an example Redux auth project that also happens to exemplify universal Redux, and realtime updating with Redux, Socket.io and RethinkDB.
From the discussion here it seems that the state of Redux reducers should be persisted in a database.
To persist the state or not, it's likely not a concern of Redux at all. It's more up to application logic.
If something happens in an application, like data upload to server, obviously you need to save state (or a slice of the state to a server).
Since network calls are asynchronous, but Redux is synchronous - you need to introduce additional middleware, as redux-thunk or redux-promise.
As sign-up example, you likely need that actions,
export function creatingAccount() {
return { type: 'CREATING_ACCOUNT' };
}
export function accountCreated(account) {
return { type: 'ACCOUNT_CREATED', payload: account };
}
export function accountCreatingFailed(error) {
return { type: 'ACCOUNT_CREATING_FAILED', payload: error };
}
export function createAccount(data, redirectParam) {
return (dispatch) => {
dispatch(creatingAccount());
const url = config.apiUrl + '/auth/signup';
fetch(url).post({ body: data })
.then(account => {
dispatch(accountCreated(account));
})
.catch(err => {
dispatch(accountCreatingFailed(err));
});
};
}
Some portion of state, e.g. user object after authorization, might be stored to localStore and re-hydrated on next application run.
Those are valid concerns. Using localStorage to persist state on the frontend might be a better strategy. You can implement this using middleware, for example:
import {createStore, compose, applyMiddleware} from 'redux';
const localStorageMiddleware = ({getState}) => {
return (next) => (action) => {
const result = next(action);
localStorage.setItem('applicationState', JSON.stringify(
getState()
));
return result;
};
};
const store = compose(
applyMiddleware(
localStorageMiddleware
)
)(createStore)(
reducer,
JSON.parse(localStorage.getItem('applicationState'))
)
If you're concerned about the enemy accessing the user's laptop and stealing credentials from it you could persist state to the backend when the user leaves the page (Navigator.sendBeacon() might be helpful here) & store it in the session.