react-router-dom and redux subapp: useNavigator causes redux reload - reactjs

I'm using react-router-dom v6. I'm versed with redux and react but only just starting to "go deep" with react-router-dom.
I have nested routes. One of the "nests" has access to the redux <Provider> context.
Despite navigating within the "nest" of Routes with access to the redux context, all of the rendered components in the Router context are being re-rendered - and with it resetting redux.
Finally, there are several reasons for this design, but one of them is so that I can initialize the middleware for a given project (specified in the url).
const middleware = (projectId) => {
let initialized = false;
return (store) => (next) => (action) => {
if (!initialized) {
initialized = true;
next({ type: "SET_READY", projectId });
}
next(action);
};
};
Here is a link to the sandbox that highlights the issue.

Thank you #DrewReese for engaging me on this issue.
In the sandboxed description of the issue, I solve the issue by memoizing the function that instantiates the store. The middleware has a two-step initialization process: (1) set the project id (2) load the store. The store value in the second step depends on what is returned by the server (i.e., if null, use a new store).
// core-app.jsx
// Three versions of the store instantiation process
// 1. resets store on every new page within the project detail view (bad)
// const storeWithProject = store(projectId);
// 2. ##INIT instantiates using initialState where projectId = null (see reducer)
// const storeWithProject = useMemo(() => store(projectId), [projectId]);
// 3. The comprehensive solution
const storeWithProject = useMemo(
() => store(projectId, seedState(projectId)),
[projectId]
);
This all said, in the actual app, I wasn't able to get use the redux <Provider> within the <BrowserRouter> context. I was not able to prevent a re-render of the SubApp component with every route change (as described above); my reliance on useEffect to fetch data (e.g., list of projects) at various points in the app is beyond my current skill-set :-/ (prop and state changes in context of react-router is tough-stuff to manage).
The solution that I'm working on now is to "raise" the redux provider to minimize the number of parents that might trigger a re-render. A somewhat disappointing conclusion because I can't leverage the "natural" entry point for when to instantiate the redux store.

Related

Saving and restoring the full state of a React app

I'm building a VSCode extension using React. When the tab loses focus, VSCode shuts down the WebView and only reloads it when the tab gets focus again. The app fully reloads from the start.
VSCode already provides a way to save arbitrary state object and then get it back when restoring the WebView.
What remains is to serialize the full state of the React app (the whole React DOM, states etc) into a simple JSON-like object.
How can I serialize the full state of the React app and then reload it?
I know that React has some features like Server-Side Rendering - maybe they can be used to serialize DOM and state?
To accomplish that, you need some kind of global state object, which holds all the state data that you want to preserve. You can then save and restore this object using the VSCode API you mentioned.
There are several ways to do that and different 3rd-party libraries for this purpose. Here I will outline some of the options.
Context API
Context API is built into React. You need to create an instance and wrap your app with a context provider. Then you can access the state in your child components with useContext.
Here's an example of how you would use it to store some user and page data, as well as control some textarea field in a child component, which would normally be a local state.
const App = () => {
const [user, setUser] = useState();
const [textAreaValue, setTextAreaValue] = useState("");
const [currentPage, setCurrentPage] = useState("home");
// etc.
// this is your global state object that you can then save using VSCode magic
const globalState = { user, setUser, /* etc. */ };
return (
<GlobalStateContext.Provider value={globalState}>
<Child />
</GlobalStateContext.Provider>
);
}
...
const Child = () => {
const { textAreaValue, setTextAreaValue } = useContext(GlobalStateContext);
const handleChange = (e) => {
setTextAreaValue(e.target.value);
}
return (
<textarea value={textAreaValue} onChange={handleChange} />
);
}
Of course, this will be cumbersome if you have a lot of state data to manage. Furthermore, whenever any field in the context changes, all components using it will re-render. This could cause performance issues, so this solution does not scale well. It should be fine for a simple application though.
Custom store hook
Another solution would be to use a global store functionality. You could write a custom hook for that and then use it like this:
const Child = () => {
const { textAreaValue, setTextAreaValue } = useStore("textarea");
const handleChange = (e) => {
setTextAreaValue(e.target.value);
}
return (
<textarea value={textAreaValue} onChange={handleChange} />
);
}
I won't provide a full example of how to implement this for brevity, but here is one guide that could be useful.
3rd-party library
There are also 3rd-party libraries that implement the global store functionality. A popular choice is Redux, although I personally wouldn't recommend it if you haven't used it before, due to its verbosity and somewhat of a learning curve. Other options include Recoil, react-hooks-global-state and ReactN.

In Next.js, how can I update React Context state with data from getServerSideProps?

I'm having a lot of trouble learning to properly load data into state in my todo app.
I have a next.js page component pages/index.tsx where I load data from my API via getServerSideProps and return it as a page prop called tasksData.
The tasksData is being returned properly and I can access them in my page component just fine via prop destructuring: const Home = ({ tasksData }: Home) => { }
I also have a React Context provider in _app.tsx called BoardProvider. This stores state for my task board, and employs useReducer() from the React Context API to update this state in context consumers, such as pages/index.tsx.
The challenge I am facing is how to make my UI's "source of truth" the state stored in my context provider (eg. const { { tasks }, dispatch } = useBoard();, rather than the page page props returned from my API (eg. the tasksData prop).
One approach I considered was to simply load the data in getServerSideProps and then set the state via a dispatched action in a useEffect hook:
useEffect(() => {
// On first render only, set the Context Provider state with data from my page props.
dispatch({ type: TaskAction.SET_TASKS, payload: tasksData });
});
However, this doesn't seem to be working because sometimes tasksData is undefined, presumably because Next.js has not yet made it available on page mount.
Another suggestion I heard was to fetch the data and pass it as pageProps to my Context Provider in _app.tsx. I believe this means using getInitialProps() in _app.tsx so that my provider's initial state is populated by my API. However, this disabled static optimization and other useful features.
Can anyone help me out with some pseudocode, documentation, or examples of how to use getServerSideProps in combination with React Context API?
Couple of points:
getServerSideProps should be invoked before the page is even rendered. So theoretically your tasksData is undefined is a bug! You can't have a server data to be unavailable unless you really really intend to have that happen in the first place.
Assuming getServerSideProps is always returning the right data, but you want to use your own data to override it. In your context, you can have this logic.
const Home = ({ tasksData }) => {
const value = { tasksData: {
// let me override it
}}
return (
<Context.Provider value={value}>
...
<Context.Provider>
)
}
If you have the context provided under a page, the above code is all you need. But if your context is provided in a root (parent of a page), you can still add the above code to re-provide the same context again with overridden value. Because this is how a context is designed, read https://javascript.plainenglish.io/react-context-is-a-global-variable-b4b049812028 for more info.

How to use a react hook in an arrow function?

I'm designing an application in react native for medical use case. The idea is to record some audio and to send it to a local server. Therefore, the ip adress is defined at runtime.
Previously, I used a global const as adress (code below).
const url = 'http://192.168.43.56:5000/';
const checkIsServerOnline = async (): Promise<string> =>
axios.get(url, {
timeout: 5000,
});
Now I would like to retrieve the url from a redux (persistant) store before using the function. I tried something like that, but it didn't worked:
function CheckIsServerOnline(): AxiosPromise {
const { ipAdress } = useSelector(
(state: RootState) => state.config
);
return axios.get(ipAdress, {
timeout: 5000,
});
}
The error message it returned:
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 might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
Do you have any idea how I would be able to retrieve the value from the redux state ?
(Apologies for the begineer question)
React hooks can be used ONLY inside of React components.
Since you are using Redux I think you should set up your URL call in the store initialization. Here's a link to redux docs about initializing state: https://redux.js.org/recipes/structuring-reducers/initializing-state
Once you have it in the redux state you will be able to retrieve it wherever you want thanks to mapStateToProps from redux.

Why Redux Store is publishing changes to listeners even if state did NOT change in the store?

Following code using simple Redux API:
// Dummy Reducer that just returns previous state
const counter = (state = 0, action) => {
return state;
}
// Store
const { createStore } = Redux;
const store = createStore(counter);
const listener = () => {
console.log('Listener called...with ' + store.getState());
};
// Listener
store.subscribe(listener);
// Manually dispatching actions
store.dispatch({ type: 'DUMMY' });
store.dispatch({ type: 'DUMMY' });
store.dispatch({ type: 'DUMMY' });
produces following output:
Listener called...with 0
Listener called...with 0
Listener called...with 0
My question:
If nothing changes in the store, why listener is being notified as if something changed. Isnt is unnecessary and counter productive? Lets say, the listeners are views like React Container Components. They will try rerendering unnecessarily right ?
Or am I missing something?
In case of Flux, I feel we have higher flexibility in terms of whether to publish the change from the store or not. Is this a con for Redux over Flux ? Or am I missing something?
According to the docs, store.subscribe():
Adds a change listener. It will be called any time an action is dispatched, and some part of the state tree may potentially have changed.
Many (most?) popular redux patterns do not require developers to use store.subscribe() at all. See react-redux and redux-saga.
However, it's a good question whether store.subscribe affects performance of such frameworks.
With react-redux, the most popular redux framework for react, container components don't do long-running tasks like http requests; those are typically handled by dispatched actions. So container components tend to be very high-performance, simply pulling data out of simple objects in the store. When their output doesn't change, then the associated view components won't re-render.

Firing Redux actions in response to route transitions in React Router

I am using react-router and redux in my latest app and I'm facing a couple of issues relating to state changes required based on the current url params and queries.
Basically I have a component that needs to update it's state every time the url changes. State is being passed in through props by redux with the decorator like so
#connect(state => ({
campaigngroups: state.jobresults.campaigngroups,
error: state.jobresults.error,
loading: state.jobresults.loading
}))
At the moment I am using the componentWillReceiveProps lifecycle method to respond to the url changes coming from react-router since react-router will pass new props to the handler when the url changes in this.props.params and this.props.query - the main issue with this approach is that I am firing an action in this method to update the state - which then goes and passes new props the component which will trigger the same lifecycle method again - so basically creating an endless loop, currently I am setting a state variable to stop this from happening.
componentWillReceiveProps(nextProps) {
if (this.state.shouldupdate) {
let { slug } = nextProps.params;
let { citizenships, discipline, workright, location } = nextProps.query;
const params = { slug, discipline, workright, location };
let filters = this._getFilters(params);
// set the state accroding to the filters in the url
this._setState(params);
// trigger the action to refill the stores
this.actions.loadCampaignGroups(filters);
}
}
Is there a standard approach to trigger actions base on route transitions OR can I have the state of the store directly connected to the state of the component instead of passing it in through props? I have tried to use willTransitionTo static method but I don't have access to the this.props.dispatch there.
Alright I eventually found an answer on the redux's github page so will post it here. Hope it saves somebody some pain.
#deowk There are two parts to this problem, I'd say. The first is that componentWillReceiveProps() is not an ideal way for responding to state changes — mostly because it forces you to think imperatively, instead of reactively like we do with Redux. The solution is to store your current router information (location, params, query) inside your store. Then all your state is in the same place, and you can subscribe to it using the same Redux API as the rest of your data.
The trick is to create an action type that fires whenever the router location changes. This is easy in the upcoming 1.0 version of React Router:
// routeLocationDidUpdate() is an action creator
// Only call it from here, nowhere else
BrowserHistory.listen(location => dispatch(routeLocationDidUpdate(location)));
Now your store state will always be in sync with the router state. That fixes the need to manually react to query param changes and setState() in your component above — just use Redux's Connector.
<Connector select={state => ({ filter: getFilters(store.router.params) })} />
The second part of the problem is you need a way to react to Redux state changes outside of the view layer, say to fire an action in response to a route change. You can continue to use componentWillReceiveProps for simple cases like the one you describe, if you wish.
For anything more complicated, though, I recommending using RxJS if you're open to it. This is exactly what observables are designed for — reactive data flow.
To do this in Redux, first create an observable sequence of store states. You can do this using rx's observableFromStore().
EDIT AS SUGGESTED BY CNP
import { Observable } from 'rx'
function observableFromStore(store) {
return Observable.create(observer =>
store.subscribe(() => observer.onNext(store.getState()))
)
}
Then it's just a matter of using observable operators to subscribe to specific state changes. Here's an example of re-directing from a login page after a successful login:
const didLogin$ = state$
.distinctUntilChanged(state => !state.loggedIn && state.router.path === '/login')
.filter(state => state.loggedIn && state.router.path === '/login');
didLogin$.subscribe({
router.transitionTo('/success');
});
This implementation is much simpler than the same functionality using imperative patterns like componentDidReceiveProps().
As mentioned before, the solution has two parts:
1) Link the routing information to the state
For that, all you have to do is to setup react-router-redux. Follow the instructions and you'll be fine.
After everything is set, you should have a routing state, like this:
2) Observe routing changes and trigger your actions
Somewhere in your code you should have something like this now:
// find this piece of code
export default function configureStore(initialState) {
// the logic for configuring your store goes here
let store = createStore(...);
// we need to bind the observer to the store <<here>>
}
What you want to do is to observe changes in the store, so you can dispatch actions when something changes.
As #deowk mentioned, you can use rx, or you can write your own observer:
reduxStoreObserver.js
var currentValue;
/**
* Observes changes in the Redux store and calls onChange when the state changes
* #param store The Redux store
* #param selector A function that should return what you are observing. Example: (state) => state.routing.locationBeforeTransitions;
* #param onChange A function called when the observable state changed. Params are store, previousValue and currentValue
*/
export default function observe(store, selector, onChange) {
if (!store) throw Error('\'store\' should be truthy');
if (!selector) throw Error('\'selector\' should be truthy');
store.subscribe(() => {
let previousValue = currentValue;
try {
currentValue = selector(store.getState());
}
catch(ex) {
// the selector could not get the value. Maybe because of a null reference. Let's assume undefined
currentValue = undefined;
}
if (previousValue !== currentValue) {
onChange(store, previousValue, currentValue);
}
});
}
Now, all you have to do is to use the reduxStoreObserver.js we just wrote to observe changes:
import observe from './reduxStoreObserver.js';
export default function configureStore(initialState) {
// the logic for configuring your store goes here
let store = createStore(...);
observe(store,
//if THIS changes, we the CALLBACK will be called
state => state.routing.locationBeforeTransitions.search,
(store, previousValue, currentValue) => console.log('Some property changed from ', previousValue, 'to', currentValue)
);
}
The above code makes our function to be called every time locationBeforeTransitions.search changes in the state (as a result of the user navigating). If you want, you can observe que query string and so forth.
If you want to trigger an action as a result of routing changes, all you have to do is store.dispatch(yourAction) inside the handler.

Resources