SSR React with data for redux store - reactjs

How possible is it to populate store (on server side) with already fetched data without triggering API (dispatch(action))?
The steps:
Client going to website with link /users
On the server I take /users and pass it to the StaticRouter
On this step the documentation tells that, I need to dispatch all actions (aka function loadData) in all components (specific for this route) recursively and store fetched data in redux store.
But I already have all necessary data for this and I don't want to trigger API.
Thanks in advance for any help.

You could construct the application state manually using existing data, and create the redux store from that application state, and pass it over to the client side in a global variable.
The client side then would read the state and rehydrate the redux store. The following is an untested illustration of this:
Server Side
// ...
// import stuff from places
// ...
// Readily available user data
const users = [{ "_id": 1000, "name": "John Doe" }, { "_id": 2000, "name": "Jane Brown" }];
app.get('/users', (req, res) => {
const context = {};
// Manually construct the redux state
const state = { users: users };
// Create redux store with existing state
const store = createStore(
JSON.stringify(state)
);
const reactDomString = renderToString(
<Provider store={store}>
<StaticRouter context={context} location={req.url}>
<App />
</StaticRouter>
</Provider>
);
res.end(`
<div id="app">${ reactDomString }</div>
<script>
window.REDUX_DATA = ${ JSON.stringify(state) }
</script>
<script src="./app.js"></script>
`);
});
Client Side
// ...
// import stuff from places
// ...
// Pick up the data from the SSR template
const store = createStore(window.REDUX_DATA);
const appContainer = document.getElementById("app");
ReactDOM.hydrate(
<ReduxProvider store={store}>
<Router>
<Layout />
</Router>
</ReduxProvider>,
appContainer
);

Related

Connecting to a redux store from outside an iframe

I have a next.js application which uses context for state management, we are are moving to redux as context triggers a re-render on on all consumers of a context provider whose value is updated... and we use a global context provider. We are unable to change to co-located context providers as requires a lot of work / we have been burnt by context.
So redux.
I build redux store, and in local development - at app level i see redux tool kit, the actions and state being updated.
const buildStore = () => {
const devMode = process.env.NODE_ENV === 'development';
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
// https://redux-toolkit.js.org/api/configureStore
reducer: rootReducer,
middleware: (getDefaultMiddleware) => [
...getDefaultMiddleware({
thunk: false, // we use redux-saga, not the default, thunk
serializableCheck: false, // redux toolkit issues
}),
sagaMiddleware,
],
devTools: devMode, // enable devTools in dev env
});
sagaMiddleware.run(rootSaga);
return store;
};
ie
const Widget = ({ formSettings }) => {
const client = getClient();
const store = buildStore();
return (
<ReduxProvider store={store}>
<StateProvider formSettings={formSettings}>
<ApolloProvider client={client}>
<ThemeProvider>
<Form />
<BrowserMessages />
</ThemeProvider>
</ApolloProvider>
</StateProvider>
</ReduxProvider>
);
};
Yes we also have apollo, which has a redux store internally. It is our intention to remove context.
Issue i have is that this application is built within an iframe, so when i load the app locally (within iframe) its showing the redux actions, but if i load the application in staging then we are outside the iframe and i do not see the action in RTK or store being updated.. although can seen the network request which is triggered by browser messages, so i know that the application is working as expected (and the component value in the context provider has been updated).
How do i connect RTK to the store in the iframe?

React testing: Session storage returning empty value inside the rendered component after setting value using mocked session storage

I am trying to test the Callback part of the OAuth flow in React. I am rendering the callback component with the code and state in the URL and then setting sessionStorage values using mock-local-storage library. The issue is that I am unable to read those values inside the component using .getItem(...)
Jest.config.js
module.exports = {
setupFilesAfterEnv: ["./src/setupTests", "mock-local-storage"],
setupFiles: ["mock-local-storage"],
};
Test
it("should get token after getting code from the URL", async () => {
const history = createMemoryHistory();
render(
<Router history={history}>
<AuthProvider>
<AlertProvider template={AlertTemplate}>
<Callback />
</AlertProvider>
</AuthProvider>
</Router>
);
const state = "randomvalue";
sessionStorage.setItem("login", "true");
sessionStorage.setItem("state", state);
sessionStorage.setItem("verifier", "1234");
sessionStorage.setItem("redirect_on_login", "/");
history.push({
pathname: `/callback`,
search: `?code=5abab343&state=${state}`,
});
expect(history.location.pathname).toBe("/callback");
screen.debug();
// await waitFor(() => expect(sessionStorage.getItem("state")).toBeTruthy());
});
Component lines being tested:
const auth_code = search.get(CODE);
const state = search.get(STATE);
console.log("url state ", state) // prints correct URL state
console.log("sess state ", sessionStorage) // prints {}
I want sessionStorage to print the values I set in the test. Please help. Thanks in advance!

React SSR : How can my component get the URL search URL parameters in its render method

I am unclear how to get the request's search parameters to be available in a component when reading React apps on the server.
Client Side Rendering
In a Client side rendered React application I know how to get the request's search parameters passed onto the component used for that route.
The code below is in my CSR app. See the 2nd route, I am getting the value of the search param "b" and passing it onto my component as the "name" property.
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route
exact
path="/a/:id"
render={(props) => {
const params = new URLSearchParams(props.location.search);
const b = params.get('b');
return ( <MyComp id={props.match.params.id} name={b} /> );
}}
/>
</Switch>
</Router>
Therefore if my URL was
http://host:port/a/b=George
MyComp's
this.props.b
will return "George". The component can get this value in its "render" method
render(){
const { name } = this.props;
}
Server Side Rendering
In my SSR app the routes are defined using a route config
export default [
{
...Home,
path: '/',
exact: true,
title: 'Home Screen',
},
{
...MyComp,
path: '/a/:id',
exact: true,
title: 'My Component'
},
];
There is no props available here, so unlike the CSR application I can not get the "props.location.search" and send to the component.
This Routes object is used in my server and client renderers:
<Provider store={store}>
<StaticRouter .... }>
<div>{renderRoutes(Routes)}</div>
</StaticRouter>
</Provider>
<Provider store={store}>
<BrowserRouter ....>
<div>{renderRoutes(Routes)}</div>
</BrowserRouter>
</Provider>,
In my component "render" method, I have no way to get the value of the "b" parameter. "this.props.location" does not have any value in its "search" property.
How to do make the search url parameters available to my components render method in a React SSR app.
I did wonder if could do something like the following, but obviously "props" is not known in this context
{
...MyComp,
path: '/a/:id',
exact: true,
title: 'My Component',
name: new URLSearchParams(props.location.search).get('b')
},
I know i can get the information in my Express Server code from the "req.query". But all i can do with that value is pass it to my components "loadData" method, but this does not make it available in my render method.
server.get('*', (req, res) => {
const store = createStore();
console.log(req.query) // this gives me the value of "b"
// I could pass req.query to the component's loadData but that does not gain me anything
const promises = matchRoutes(Routes, req.path)
.map(({ route, match }) => (
route.loadData ? route.loadData(store, match, req.query) : null)
);
Any ideas how to make the search URL parameters available to my route component's render method?
I have found a solution to this issue. I am not sure its the best solution but it appears to work in both server and client side rendering.
Server Side
On the server side, in the Express Server file, add the "req.query" to each Route in the Routes object just before its used in MatchRoutes
import Routes from '../pages/Routes';
...
server.get('*', (req, res) => {
Routes.forEach((r)=>{
r.queryParams = req.query;
});
const promises = matchRoutes(Routes, req.path)
....
This makes the "queryParams" available in the component when rendering server side, so the value of "b" query param can be obtained
render() {
const { route } = this.props;
const { queryParams } = route;
const name = queryParams.b;
....
Client Side
The client side Routes will not have the "queryParams" populated, but "this.props.location.search" has the values so we can get the value from there.
componentDidMount() {
const { location } = this.props;
const params = new URLSearchParams(location.search);
const name = params.get('b');
this.setState({ name });
Rendering the name
In the component's render method you have to check if its being rendered client side or server side and use the relevant data.
render() {
// server side rendering values
const { route } = this.props;
const { queryParams } = route;
// client side rendering values
const { name } = this.state;
let title = name; // will be undefined in SSR
if (queryParams){
// SSR set the name
title = queryParams.b;
}
Another solution for the server side part is to add the req.query to the context object which contains all the data created when calling the matched route's components method.
const context = { data, requestQueryParams: req.query };
This context object is then included on the server side router
<StaticRouter context={context} ...>
Components can then get any data from this context object when on the server side
const { staticContext } = this.props;
data = staticContext.data;
const myQueryParamValue = staticContext.requestQueryParams.myQueryParamName;

How to set initial redux state from asynchronous source in react native?

I trying to load all the asynchronous data like user objects from asynchronous storage when the store first-time initialize. But I do not have any idea how to do that. I got one of the suggestions to create a middleware to do this but I don't know-how.
Current Store and its initialization in app
const store = createStore(rootReducer, {}, applyMiddleware(ReduxThunk,
//actionCallbackOnceMiddleware(INITIAL_AJAX_END, AppSafeAreaProvider)
))
const AppSafeAreaProvider = () => {
return <Provider store={store}>
<PaperProvider>
<SafeAreaProvider>
<App />
</SafeAreaProvider>
</PaperProvider>
</Provider>
}
AppRegistry.registerComponent(appName, () => AppSafeAreaProvider);
I'm not sure about the method for setting up redux on React Native since I only use React.js but if you are going to setup your store to handle asynchronous call handlers, your best bet would be to look into middlewares.
https://redux.js.org/api/applymiddleware
Here is an excellent example of how the async requests can be called using the middleware and redux-thunk
https://redux.js.org/advanced/async-actions
At the end of the example, it shows how the redux-thunk can be used to initialize your store with an async API call.
You don't need default state globally because reducers already have a default state parameter on redux
For example
const someReducer = (defaultState = {foo: 'bar'}, action) => {
switch(action.parameter)
{
...
default return defaultState
}
}
Your state
{
someReducer: { foo: 'bar' }
}

How to prevent redux-persist from using LocalStorage before it has been allowed by the user?

I would like to get the user's consent with a click on a banner button before I use LocalStorage. LocalStorage is used through redux-persist. I'm using redux and redux-persist as follows:
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persitor} >
<MyAppRouter />
</PersistGate>
</Provider>,
document.getElementById("root")
)
and store and persistor are coming from
import { createStore } from "redux"
import { persistStore, persistReducer } from "redux-persist"
import storage from "redux-persist/lib/storage"
import { rootReducer } from "../reducers/index"
const persistConfig = {
key: "root",
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = createStore(persistedReducer)
const persistor = persistStore(store as any)
// `as any` is necessary as long as https://github.com/reduxjs/redux/issues/2709 is not fixed
export { store, persistor }
redux-persist persists the initial state in LocalStorage as soon as the objects are created which is not what I want.
I'd like to use redux in my React components no matter whether the state is persisted or not.
I assume the details of the reducer don't matter as it doesn't have influence on where/how the state is stored.
The information that the user once has expressed consent to use LocalStorage and cookies is stored in a cookie.
How could I start using LocalStorage for persistence only after the user has expressed consent during the first visit or if the cookie is present already? The case that the cookie expired or has been deleted should be covered by the case of the first visit.
In order to minimize discussion I'm looking for a solution which solves the technical problem. The request might be overkill in terms of legal requirements.
Things I tried so far:
Use two store (probably bad practice) which are managed in the state of an App component which wraps Provider as shown in the example. Problem is that the page is re-rendered as soon as the banner button is clicked which is not acceptable.
Evaluated blacklisting which won't work because it has no impact on the initial persistence of the state.
[1] https://softwareengineering.stackexchange.com/questions/290566/is-localstorage-under-the-cookie-law
The idea here is that redux-persist's persistor component is responsible for persisting data or redux state in localStorage.
In order to make a decision based on the user's consent, you need to conditionally render or not the PersistGate component
To solve such a problem you can write a custom component over Persistor which renders it only when the permission is granted. Also the logic to prompt the user to grant or deny permission can go in the same component
Example
class PermissionAndPersist extends React.Component {
constructor(props) {
super(props)
this.state = {
permission: this.getCookie('userLocalStoragePermission')
}
}
getCookie(name) {
//implement the getc ookie method using document.cookie or a library
/* state returned must have the following syntax
isPermissionGranted,
isCookieExpired
*/
// The above syntax can be determined based on whether cookie is present as well as by checking the expiry data if cookie was present
}
render() {
const { permission } = this.state;
const {children, persistor} = this.props;
if(!permission.isPermissionGranted) {
// no permission granted, return a plain div/Fragment
return (
<React.Fragment>
{children}
</React.Fragment>
)
}
if(permission.isCookieExpired) {
return <Modal>{/* here goes a component which asks for user permission and on click updates the state as well as update in cookie */}</Modal>
}
// now if the cookie is present and permission is granted and cookie is not expired you render the `PersistGate` component
return <PersistGate persistor={persitor} >{children}</PersistGate>
}
}
Once you create the component as above you will render it as follows
ReactDOM.render(
<Provider store={store}>
<PermissionAndPersist persistor={persitor} >
<MyAppRouter />
</PermissionAndPersist >
</Provider>,
document.getElementById("root")
)
Note: You can always modify the implementation of PermissionAndPersist component depending on what the requirement is, but note that the PeristGate must only be rendered when all conditions are matching. Also you might want to clear localStorage if the user doesn't grant permission
EDIT: Since the requirement is to actually not re-render the entire app on click of user banner, we need to make a few changes.
Firstly, re re-render the ModalComponent based on a condition. Secondly, we cannot conditionally change the component we re-render or else the entire app is refreshed. The only way to achieve it as of now is to actually implement the logic to persist redux state in localStorage yourself and fetch it initially on refresh
class PermissionAndPersist extends React.Component {
constructor(props) {
super(props)
this.state = {
permission: this.getCookie('userLocalStoragePermission')
}
}
getCookie(name) {
//implement the getc ookie method using document.cookie or a library
/* state returned must have the following syntax
isPermissionGranted,
isCookieExpired
*/
// The above syntax can be determined based on whether cookie is present as well as by checking the expiry data if cookie was present
}
componenDidMount() {
const { permission } = this.state;
const { dispatch } = this.props;
if(permission.isPermissionGranted && !permission.isCookieExpired) {
// Idea here is to populate the redux store based on localStorage value
const state= JSON.parse(localStorage.get('REDUX_KEY'));
dispatch({type: 'PERSISTOR_HYDRATE', payload: state})
}
// Adding a listner on window onLoad
window.addEventListener('unload', (event) => {
this.persistStateInLocalStorage();
});
}
persistStateInLocalStorage = () => {
const { storeState} = this.props;
const {permission} = this.state;
if(permission.isPermissionGranted && !permission.isCookieExpired) {
localStorage.set('REDUX_KEY', JSON.stringify(storeState))
}
}
componentWillUnmount() {
this.persistStateInLocalStorage();
}
render() {
const {children} = this.props;
const {permission} = this.state;
return (
<React.Fragment>
{children}
{permission.isCookieExpired ? <Modal>{/*Pemission handling here*/}</Modal>}
</React.Fragment>
)
}
const mapStateToProps = (state) => {
return {
storeState: state
}
}
export default connect(mapStateToProps)(PermissionAndPersist);
Once you implement the above component, you need to listen to the PERSISTOR_HYDRATE action in reducer and update the redux state.
NOTE: You might need to add more handling to make the persist and rehydrate correctly, but the idea remains the same
The docs for persistStore say that you can pass PersistorOptions like {manualPersist: true}, which won't persist until later calling persistor.persist(). Note that anything you've put inside a PersistGate will be gated until you start persisting.
// cookieConsent.js
import CookieConsent from "#grrr/cookie-consent"
export const BASIC_FUNCTIONALITY = 'basic-functionality';
export const cookieConsent = CookieConsent({
...
cookies: [
{
id: BASIC_FUNCTIONALITY,
...
},
...
]
});
// configureStore.js
import {cookieConsent, BASIC_FUNCTIONALITY} from './cookieConsent'
...
export default function configureStore(initialState) {
...
const persistedReducer = persistReducer({
key: 'root',
storage,
serialize,
whitelist: config.reduxPersistWhitelist,
}, createRootReducer(history))
const store = createStore(...)
const persistor = persistStore(store, {manualPersist: true} as PersistorOptions)
cookieConsent.on('update', (cookies) => {
forEach(cookies, (cookie) => {
switch (cookie.id) {
case APP_STATE_COOKIE_ID:
if (cookie.accepted) {
persistor.persist();
} else {
persistor.pause();
persistor.purge().catch(console.log);
}
break;
}
}
})
...
}

Resources