Basic setup:
1) App is React and Redux,
2) App is served by a front facing NGINX serving static files like html, pictures and of course the app itself. It also forwards all relevant requests (web sockets and/or AJAX) to the back end (phoenix/elixir).
3) Users are required to authenticate. I'm using redux-oidc library, which is client side only and it works fine.
4) After user logs on is when I get hazy on what to do next.
Question(s):
1) I can't send the state along with the first request because I don't know who the user is and, thus, don't know which state to send. Meanwhile application is already booted (empty store created, login component displayed),
2) After user logs on I can't show anything (like user specific nav bar, timeline , mailbox) I have to saturate the store and let react do its job. What approach should I take?
3) Server rendering is out because a) I'm not using Node and rendering react components using chosen framework is messy and complicated at best and b) I won't be able to export the app to NGinx since it only serves static assets and there is no server logic run there. I could, theoretically, get rid of NGinx, have server based login on the API server and send down HTML along with JSON state which could be used to render the app on the client. However, NGinx does not only serve static assets but also load balances few instances and, thus, getting rid of it is not something I want to do.
Any advice would be appreciated.
Hydrating the state after the store was created, can be achieved by creating a main reducer that can bypass the top level reducers, and replace the whole state.
Reducers are functions that get the current state, combine it with the payload of an action, and return a new state. Usually the main reducer is a combination of all top reducers using combineReducers, and the state is the combination of state pieces returned by the top level reducers.
However, the main reducer can react to actions directly. If the main reducer receives a certain action (hydrate), instead of calling the combined reducers, it returns the action's payload (the saved state). Other actions are passed to the combined reducers.
const mainReducer = (state = {}, action) =>
action.type === 'hydrate' ?
action.payload // hydrate the state
:
reducers(state, action); // create new state by using combined reducers
Working example:
const { combineReducers, createStore } = Redux;
const people = (state = [], action) => action.type === 'people' ? [...state, action.payload] : state;
const items = (state = [], action) => action.type === 'items' ? [...state, action.payload] : state;
const reducers = combineReducers({
people,
items
});
const mainReducer = (state = {}, action) => action.type === 'hydrate' ? action.payload : reducers(state, action);
const store = createStore(mainReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'people', payload: 5 });
store.dispatch({ type: 'items', payload: 'green' });
store.dispatch({ type: 'hydrate', payload: {
people: [20, 30, 50, 100],
items: ['green', 'yellow', 'red']
}});
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.min.js"></script>
Even though the accepted answer might do the trick, I think that what you are chasing is an anti-pattern, after all.
If you are doing client rendering and handling authentication client side, the only thing you should send to your client, in my opinion, is a <Spinner/>, with zero preloaded state.
Once you are on the client, initialise your store, do your authentication, and decide if you are going to fetch data and render the authenticated version of the page, or if user is not authenticated yet, show them the login form. Everything from this point on, should be handled client side.
Here is what Redux has to say about it:
https://redux.js.org/usage/server-rendering
So, if during the initial render you got "Schrödinger" user (might be authenticated or not), the only thing you can safely assume to show is a spinner.
ANOTHER OPTION
If you really need to get new preloaded data from the server on multiple requests (that's kind of what happens in NextJS apps).
If you are going to pre-render every page (and get new state that on every page change), you can do what NextJS suggests you to do:
Here is the example:
https://github.com/vercel/next.js/tree/canary/examples/with-redux-thunk
And the main part of their code:
import { useMemo } from 'react'
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import reducers from './reducers'
let store
function initStore(initialState) {
return createStore(
reducers,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
}
export const initializeStore = (preloadedState) => {
let _store = store ?? initStore(preloadedState)
// After navigating to a page with an initial Redux state, merge that state
// with the current state in the store, and create a new store
if (preloadedState && store) {
_store = initStore({
...store.getState(),
...preloadedState,
})
// Reset the current store
store = undefined
}
// For SSG and SSR always create a new store
if (typeof window === 'undefined') return _store
// Create the store once in the client
if (!store) store = _store
return _store
}
export function useStore(initialState) {
const store = useMemo(() => initializeStore(initialState), [initialState])
return store
}