How do I implement Sibling Component communication in App shell and SSR - javascript

I have adopted a project that was built on this starter kit. This architecture employs App Shell and SSR. I am trying to add a simple search bar and this will mean passing the search keys from the search bar component to the post-list component so they can be filtered. I have found that this is nearly impossible with Context Providers and Consumers. I would like to use Context, but I do not know how to do it. It looks like this starter kit has this as a serious shortcoming and if it could be solved, it would make this kit more useful online.
If you look at the code below and in the link above, you can see that there is a header center and then thee are pages. I need a communication between the header and the pages. You can just use the code in the link to add the sibbling communication.
The use of Hydrate seems to preclude the simple application of a context provider. Hydrate adds components in a parallel way with no way to have the Context Provider above both of them. This pattern I am using here does not work. When I update the provider it does not cause a re-render of the context consumer.
If I have to use something other than Context, like say Redux, then I will accept that answer.
Here is the client entry point:
import { onPageLoad } from 'meteor/server-render';
import MeteorLoadable from 'meteor/nemms:meteor-react-loadable';
import { Switch, Route, Router, Redirect } from 'react-router-dom';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider } from 'react-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { onError } from 'apollo-link-error';
import apolloLogger from 'apollo-link-logger';
import { onTokenChange, getLoginToken } from '/app/ui/apollo-client/auth';
import createBrowserHistory from 'history/createBrowserHistory';
import 'unfetch/polyfill';
// Initialise react-intl
import { primaryLocale, otherLocales } from '/app/intl';
// Need to preload of list of loadable components for MeteorLoadable
import '/app/ui/loadables';
// To get started, create an ApolloClient instance and point it at your GraphQL
// server. By default, this client will send queries to the '/graphql' endpoint
// on the same host.
// To avoid asynchronously accessing local storage for every GraphQL request,
// we cache the authorisation token, and update it using an onTokenChange callback
let authToken;
let authTokenInitialised = false;
onTokenChange(({ token }) => { authToken = token; authTokenInitialised = true; });
const withAuthToken = setContext(() => {
if (authTokenInitialised) {
return authToken ? { headers: { authorization: authToken } } : undefined;
}
return getLoginToken()
.then((token) => {
authToken = token;
authTokenInitialised = true;
return authToken ? { headers: { authorization: authToken } } : undefined;
});
});
const resetAuthToken = onError(({ networkError }) => {
if (networkError && networkError.statusCode === 401) {
// Remove cached token on 401 from the server
authToken = null;
authTokenInitialised = false;
}
});
const onErrorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) => console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
));
}
if (networkError) console.log(`[Network error]: ${networkError}`);
});
const client = new ApolloClient({
link: ApolloLink.from([
apolloLogger,
withAuthToken,
resetAuthToken,
onErrorLink,
new HttpLink({
uri: '/graphql',
}),
]),
cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
});
// Inject the data into the app shell.
// If the structure's changed, ssr.js also needs updating.
async function renderAsync() {
const [
React,
{ hydrate, render },
{ default: App },
{ default: HeaderTitle },
{ default: LanguagePicker },
{ default: Routes },
{ default: Menu },
] = await Promise.all([
import('react'),
import('react-dom'),
import('/app/ui/components/smart/app'),
import('/app/ui/components/smart/header/header-title'),
import('/app/ui/components/dumb/language-picker'),
import('/app/ui/routes'),
import('/app/ui/components/smart/menu'),
MeteorLoadable.preloadComponents(),
]);
// Given that we are implementing App Shell Architecture and, therefore,
// injecting (via reactDOM.render) the Header, Menu and Main components into
// different HTML elements, we need a way to share the router 'history' among
// all three mentioned components.
// As a default, for every invocation of 'BrowserRouter', there will be new
// 'history' instance created. Then, changes in the 'history' object in one
// component won't be available in the other components. To prevent this, we are
// relying on the 'Router' component instead of 'BrowserRouter' and defining our
// custom 'history' object by means of 'createBrowserHistory' function. Said
// 'history' object is then passed to every invocation of 'Router' and therefore
// the same 'history' object will be shared among all three mentioned components.
const history = createBrowserHistory();
// Inject react app components into App's Shell
const ClientApp = ({ component }) => (
<Router history={history}>
<ApolloProvider client={client}>
<Switch>
{/* Map our locales to separate routes */}
{ otherLocales.map(locale => (
<Route
key={locale}
path={`/${locale}/`}
render={props => <App component={component} {...props} locale={locale} section="app" />}
/>
))}
{ primaryLocale && (
<Route
key={primaryLocale}
path="/"
render={props => <App component={component} {...props} locale={primaryLocale} section="app" />}
/>
)}
{/* If no valid locale is given, we redirect to same route with the preferred locale prefixed */}
<Route render={({ location }) => <Redirect to={`/${window.__PREFERRED_LOCALE__ || otherLocales[0]}${location.pathname}`} />} />
</Switch>
</ApolloProvider>
</Router>
);
render(<ClientApp component={Menu} />, document.getElementById('menu'));
hydrate(<ClientApp component={HeaderTitle} />, document.getElementById('header-title'));
hydrate(<ClientApp component={LanguagePicker} />, document.getElementById('header-lang-picker'));
hydrate(<ClientApp component={Routes} />, document.getElementById('main'));
}
onPageLoad(() => {
const renderStart = Date.now();
const startupTime = renderStart - window.performance.timing.responseStart;
console.log(`Meteor.startup took: ${startupTime}ms`);
// Register service worker
import('/app/ui/register-sw').then(() => {});
renderAsync().then(() => {
const renderTime = Date.now() - renderStart;
console.log(`renderAsync took: ${renderTime}ms`);
console.log(`Total time: ${startupTime + renderTime}ms`);
});
});

You can create a Context, lets'say AppContext
// you can create some help such as the AppProvider and the useAppContext hook
import { createContext, useContext, useState } from "react";
export const AppContext = createContext({});
export const AppProvider: any = ({ initialState = { search: "" }, children }) => {
const [search, setSearch] = useState(initialState);
return (
<AppContext.Provider value={[search, setSearch]}>
{children}
</AppContext.Provider>
)
};
export const useAppContext: any = () => useContext(AppContext);
then you can plug your provider at same level of apolloProvider
...
import {AppProvider} from './context/AppProvider';
....
<AppProvider> // optionally you can pass an initialState to you context as prop
<ApolloProvider client={client}>
// your code
</ApolloProvider>
</AppProvider>
...
and finally you can use the state (search) defined in your AppContext
import {useAppContext} from './context/AppProvider';
....
export const Component = () => {
const [search, setSearch] = useAppContext()
return <div>{search}</div>
}

Related

Getting error when I try to upgrade react-router v5 to V6

I m getting typescript error when I tried to upgraded React-router-dom v5 to v6, How can I fix this typescript error. below you can find the code Thanks in advance
`
export function withRouter(ui: React.ReactElement) {
const history = useNavigate();
const routerValues: any = {
history: undefined,
location: undefined
};
const result = (
<MemoryRouter>
{ui}
<Route
path="*"
element={({ history, location }) => {
routerValues.history = history;
routerValues.location = location;
return null;
}}
/>
</MemoryRouter>
enter image description here`
below you can find entire file code
`
import React from "react";
import { Reducer } from "#reduxjs/toolkit";
import { Provider } from "react-redux";
import { MemoryRouter, Route, useNavigate } from "react-router-dom";
import buildStore from "../redux/store";
export function withRedux(
ui: React.ReactElement,
reducer: {
[key: string]: Reducer;
},
initialState: any
) {
const store = buildStore(initialState, true);
const dispatchSpy = jest.spyOn(store, "dispatch");
return {
result: <Provider store={store}>{ui}</Provider>,
store,
dispatchSpy
};
}
export function withRouter(ui: React.ReactElement) {
const history = useNavigate();
const routerValues: any = {
history: undefined,
location: undefined
};
const result = (
<MemoryRouter>
{ui}
<Route
path="*"
element={({ history, location }) => {
routerValues.history = history;
routerValues.location = location;
return null;
}}
/>
</MemoryRouter>
);
return { result, routerValues };
}
`
I am passing history and location props which were work fine when I was using react router v5
here is the previous code :
`
const result = (
<MemoryRouter>
{ui}
<Route
path="*"
render={({ history, location }) => {
routerValues.history = history;
routerValues.location = location;
return null;
}}
/>
</MemoryRouter>
`
After update react router v6 I changed in my code because We know that v6 no longer support render keyword inside route So I Replace it
`
const result = (
<MemoryRouter>
{ui}
<Route
path="*"
element={({ history, location }) => {
routerValues.history = history;
routerValues.location = location;
return null;
}}
/>
</MemoryRouter>
);
`
But I don't have Idea in v6 How can I pass these props inside route
Try this:
export function withRouter(ui: React.ReactElement) {
const history = useNavigate();
const location = useLocation();
const routerValues: any = {
history: history,
location: location
};
const result = (
<MemoryRouter>
{ui}
</MemoryRouter>
);
return { result, routerValues };
}
Issues
The withRouter Higher Order Component/render function can't use the RRD hooks outside the router it is rendering.
react-router-dom#6 Route components don't take "route props" and the element prop takes a React.ReactNode, a.k.a. JSX. The "route props" should be passed as props to the component being rendered.
Solution
You'll need to create two components. One is a test render function that provides the MemoryRouter as a test wrapper, and the other is a correct withRouter HOC.
Example:
Create a custom render function that renders the component under test into a wrapper component that provides all the various contexts (routers, redux, etc)
import { render } from '#testing-library/react';
import { MemoryRouter } from 'react-router-dom';
const Wrappers = ({ children }) => (
<MemoryRouter>
{children}
</MemoryRouter>
);
const customRender = (ui: React.ReactElement, options: object) => {
return render(ui, { wrapper: Wrappers, ...options });
};
export { customRender as render };
See the RTL setup docs for more information on custom render functions.
Create separate withRouter HOC to only decorate older React Class components that can't use the RRD hooks directly. Here's an example Typescript implementation.
import { ComponentType } from 'react';
import {
Location,
NavigateFunction,
useLocation,
useParams
} from 'react-router-dom';
export interface WithRouterProps {
location: Location;
navigate: NavigateFunction;
params: ReturnType<typeof useParams>;
}
export const withRouter = <P extends object>(Component: ComponentType<P>) =>
(props: Omit<P, keyof WithRouterProps>) => {
const location = useLocation();
const params = useParams();
const navigate = useNavigate();
return (
<Component
{...props}
{...{ location, params, navigate }}
/>
);
};

Exposing every component in Next.js app to custom hook on pageLoad

in my Next.js app, I have a react hook that fetches the currently authenticated user, and sets it to a piece of global state. I want to run this hook once on page load, but I want it to be exposed to every component in the app
import { useState, useEffect } from 'react';
import { useQuery } from '#apollo/client';
import { GET_AUTHED_USER } from '../utils/queries';
import { useAppContext } from '../context';
export const getCurrentUser = () => {
const [isCompleted, setIsCompleted] = useState(false)
const [state, setState] = useAppContext()
const { data: authedUserData } = useQuery(GET_AUTHED_USER, {
onCompleted: () => setIsCompleted(true)
});
useEffect(() => {
Router.push('/home')
if (isCompleted) {
setState({
currentUser: authedUserData?.getAuthedUser,
isAuthed: true,
});
}
}, [isCompleted]);
return [state, setState];
_APP.js
import '../styles/reset.css'
import { AppWrapper } from '../context'
import { getCurrentUser } from '../hooks';
function MyApp({ Component, pageProps }) {
const [state] = getCurrentUser()
console.log(state) // TypeError: Invalid attempt to destructure non-iterable instance.
return (
<AppWrapper>
<Component {...pageProps} />
</AppWrapper>
)
}
export default MyApp
the hook does work in pages/index.js but that means I can only run it if the / endpoint is hit.
<AppWrapper/> is where all the values get originally defined
import { createContext, useContext, useState } from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink } from '#apollo/client';
import { setContext } from '#apollo/client/link/context';
import { getCookie } from '../utils/functions';
const AppContext = createContext();
export function AppWrapper({ children }) {
const URI = 'http://localhost:5000/graphql';
const [state, setState] = useState({
currentUser: null,
isAuthed: false,
});
const httpLink = createHttpLink({
uri: URI,
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = getCookie('JWT');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? token : '',
}
}
});
const client = new ApolloClient({
cache: new InMemoryCache(),
link: authLink.concat(httpLink)
});
return (
<AppContext.Provider value={[state, setState]}>
<ApolloProvider client={client}>
{children}
</ApolloProvider>
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
Interesting question, you want to load that portion of code only once per each browser hit?
Then the location is right. NextJs make sure when you have a unique browser hit, it runs _app.js, but only once, after that it'll goes into a single page application mode.
After the above fact, actually whether a piece of code is run only once or twice or multiple time is mostly driven by how many times it detects the "change".
useEffect(() => {
// run
}, [condition])
If the condition changes, it'll run again. However if the condition does not change, but the whole piece is re-mount, it'll run again. You have to consider both fact here.
In short, if you have to run it per route change, make the condition === route.name. A piece of advice, try work with the single page application first, then work with the unique feature nextJS, because otherwise it'll be really difficult to figure out the answer.

Why does React/redux state reset on refresh?

When i login, everything works fine but the moment i hit refresh or navigate somewhere else, the state gets reset. Any idea why? I want to be able to reference the user from the state and get info like name etc and use it inside components. But it only works right after i login and then it will reset.
also, why do i have to use .get in mapstatetoprops? If not i get a Map object. is it because of IMMUTABLE.JS?
Here's my app.js file
/**
* app.js
*
* This is the entry file for the application, only setup and boilerplate
* code.
*/
// Needed for redux-saga es6 generator support
import '#babel/polyfill';
// Import all the third party stuff
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router/immutable';
import jwt_decode from 'jwt-decode';
import FontFaceObserver from 'fontfaceobserver';
import history from 'utils/history';
import 'sanitize.css/sanitize.css';
// Import root app
import App from 'containers/App';
import './styles/layout/base.scss';
// Import Language Provider
import LanguageProvider from 'containers/LanguageProvider';
import { setCurrentUser, logoutUser } from './redux/actions/authActions';
import setAuthToken from './utils/setAuthToken';
// Load the favicon and the .htaccess file
import '!file-loader?name=[name].[ext]!../public/favicons/favicon.ico'; // eslint-disable-line
import 'file-loader?name=.htaccess!./.htaccess'; // eslint-disable-line
import configureStore from './redux/configureStore';
// Import i18n messages
import { translationMessages } from './i18n';
// Check for token to keep user logged in
if (localStorage.jwtToken) {
// Set auth token header auth
const token = JSON.parse(localStorage.jwtToken);
setAuthToken(token);
// Decode token and get user info and exp
const decoded = jwt_decode(token);
console.log(decoded);
// Set user and isAuthenticated
setCurrentUser(decoded);
// Check for expired token
const currentTime = Date.now() / 1000; // to get in milliseconds
if (decoded.exp < currentTime) {
// Logout user
logoutUser();
// Redirect to login
window.location.href = './';
}
}
// Observe loading of Open Sans (to remove open sans, remove the <link> tag in
// the index.html file and this observer)
const openSansObserver = new FontFaceObserver('Open Sans', {});
// When Open Sans is loaded, add a font-family using Open Sans to the body
openSansObserver.load().then(() => {
document.body.classList.add('fontLoaded');
});
// Create redux store with history
const initialState = {};
const store = configureStore(initialState, history);
const MOUNT_NODE = document.getElementById('app');
const render = messages => {
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={messages}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</LanguageProvider>
</Provider>,
MOUNT_NODE,
);
};
if (module.hot) {
// Hot reloadable React components and translation json files
// modules.hot.accept does not accept dynamic dependencies,
// have to be constants at compile-time
module.hot.accept(['./i18n', 'containers/App'], () => {
ReactDOM.unmountComponentAtNode(MOUNT_NODE);
render(translationMessages);
});
}
// Chunked polyfill for browsers without Intl support
if (!window.Intl) {
new Promise(resolve => {
resolve(import('intl'));
})
.then(() =>
Promise.all([import('intl/locale-data/jsonp/en.js'), import('intl/locale-data/jsonp/de.js')]),
) // eslint-disable-line prettier/prettier
.then(() => render(translationMessages))
.catch(err => {
throw err;
});
} else {
render(translationMessages);
}
// Install ServiceWorker and AppCache in the end since
// it's not most important operation and if main code fails,
// we do not want it installed
if (process.env.NODE_ENV === 'production') {
require('offline-plugin/runtime').install(); // eslint-disable-line global-require
}
index.js inside app folder
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import NotFound from 'containers/Pages/Standalone/NotFoundDedicated';
import Auth from './Auth';
import Application from './Application';
import ThemeWrapper, { AppContext } from './ThemeWrapper';
import { Login } from '../pageListAsync';
import PrivateRoute from './PrivateRoute';
const isLoggedIn = localStorage.getItem('jwtToken') !== null ? true : false;
window.__MUI_USE_NEXT_TYPOGRAPHY_VARIANTS__ = true;
console.log(localStorage);
class App extends React.Component {
render() {
return (
<ThemeWrapper>
<AppContext.Consumer>
{changeMode => (
<Switch>
<Route path="/" exact component={Login} />
<PrivateRoute isLoggedIn={isLoggedIn} exact path="/app" component={Application} />
<Route
path="/app"
render={props => <Application {...props} changeMode={changeMode} />}
/>
<Route component={Auth} />
<Route component={NotFound} />
</Switch>
)}
</AppContext.Consumer>
</ThemeWrapper>
);
}
}
export default App;
authactions.js that will setcurrentuser but it only happens once it seems and not again once i reload.
import axios from 'axios';
import jwt_decode from 'jwt-decode';
import setAuthToken from '../../utils/setAuthToken';
import { GET_ERRORS, SET_CURRENT_USER, USER_LOADING } from '../constants/authConstants';
// Login - get user token
export const loginUser = userData => dispatch => {
axios
.post('/api/total/users/login', userData)
.then(res => {
// Save to localStorage
// Set token to localStorage
const { token } = res.data;
localStorage.setItem('jwtToken', JSON.stringify(token));
// Set token to Auth header
setAuthToken(token);
// Decode token to get user data
const decoded = jwt_decode(token);
// Set current user
dispatch(setCurrentUser(decoded));
console.log('logged');
})
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data,
}),
);
};
// Set logged in user
export const setCurrentUser = decoded => {
return {
type: SET_CURRENT_USER,
payload: decoded,
};
};
// User loading
export const setUserLoading = () => {
return {
type: USER_LOADING,
};
};
// Log user out
export const logoutUser = history => dispatch => {
// Remove token from local storage
localStorage.removeItem('jwtToken);
// Remove auth header for future requests
setAuthToken(false);
// Set current user to empty object {} which will set isAuthenticated to false
dispatch(setCurrentUser({}));
// history.push('/app');
};
EDIT: When i look at redux devtools i see that the IF block does run every time it is refreshed, but the correct state doesn't seem to be passed on to the other components. The other components get the correct state the first time (isAuthenticated: true) but once i refresh, they go back to false. In redux devtools i see this every time i refresh.
main reducer
/**
* Combine all reducers in this file and export the combined reducers.
*/
import { reducer as form } from 'redux-form/immutable';
import { combineReducers } from 'redux-immutable';
import { connectRouter } from 'connected-react-router/immutable';
import history from 'utils/history';
import languageProviderReducer from 'containers/LanguageProvider/reducer';
import uiReducer from './modules/ui';
import initval from './modules/initForm';
import login from './modules/login';
import treeTable from '../containers/Tables/reducers/treeTbReducer';
import crudTable from '../containers/Tables/reducers/crudTbReducer';
import crudTableForm from '../containers/Tables/reducers/crudTbFrmReducer';
import ecommerce from '../containers/SampleApps/Ecommerce/reducers/ecommerceReducer';
import contact from '../containers/SampleApps/Contact/reducers/contactReducer';
import chat from '../containers/SampleApps/Chat/reducers/chatReducer';
import email from '../containers/SampleApps/Email/reducers/emailReducer';
import calendar from '../containers/SampleApps/Calendar/reducers/calendarReducer';
import socmed from '../containers/SampleApps/Timeline/reducers/timelineReducer';
import taskboard from '../containers/SampleApps/TaskBoard/reducers/taskboardReducer';
/**
* Branching reducers to use one reducer for many components
*/
function branchReducer(reducerFunction, reducerName) {
return (state, action) => {
const { branch } = action;
const isInitializationCall = state === undefined;
if (branch !== reducerName && !isInitializationCall) {
return state;
}
return reducerFunction(state, action);
};
}
/**
* Merges the main reducer with the router state and dynamically injected reducers
*/
export default function createReducer(injectedReducers = {}) {
const rootReducer = combineReducers({
form,
ui: uiReducer,
initval,
login,
socmed,
calendar,
ecommerce,
contact,
chat,
email,
taskboard,
treeTableArrow: branchReducer(treeTable, 'treeTableArrow'),
treeTablePM: branchReducer(treeTable, 'treeTablePM'),
crudTableDemo: branchReducer(crudTable, 'crudTableDemo'),
crudTableForm,
crudTbFrmDemo: branchReducer(crudTableForm, 'crudTbFrmDemo'),
language: languageProviderReducer,
router: connectRouter(history),
...injectedReducers,
});
// Wrap the root reducer and return a new root reducer with router state
const mergeWithRouterState = connectRouter(history);
return mergeWithRouterState(rootReducer);
}
createStore.js
/**
* Create the store with dynamic reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'connected-react-router';
import { fromJS } from 'immutable';
import createSagaMiddleware from 'redux-saga';
import thunk from 'redux-thunk';
import createReducer from './reducers';
export default function configureStore(initialState = {}, history) {
let composeEnhancers = compose;
const reduxSagaMonitorOptions = {};
// If Redux Dev Tools and Saga Dev Tools Extensions are installed, enable them
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'production' && typeof window === 'object') {
/* eslint-disable no-underscore-dangle */
if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true });
}
// NOTE: Uncomment the code below to restore support for Redux Saga
// Dev Tools once it supports redux-saga version 1.x.x
if (window.__SAGA_MONITOR_EXTENSION__)
reduxSagaMonitorOptions = {
sagaMonitor: window.__SAGA_MONITOR_EXTENSION__,
};
/* eslint-enable */
}
const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middleware = [thunk];
const middlewares = [...middleware, sagaMiddleware, routerMiddleware(history)];
const enhancers = [applyMiddleware(...middlewares)];
const store = createStore(createReducer(), fromJS(initialState), composeEnhancers(...enhancers));
// Extensions
store.runSaga = sagaMiddleware.run;
store.injectedReducers = {}; // Reducer registry
store.injectedSagas = {}; // Saga registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(createReducer(store.injectedReducers));
});
}
return store;
}
the main app file
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import NotFound from 'containers/Pages/Standalone/NotFoundDedicated';
import jwtDecode from 'jwt-decode';
import configureStore from '../../redux/configureStore';
import { setCurrentUser, logoutUser } from '../../redux/actions/authActions';
import setAuthToken from '../../utils/setAuthToken';
import Auth from './Auth';
import Application from './Application';
import ThemeWrapper, { AppContext } from './ThemeWrapper';
import { PrivateRoute } from './PrivateRoute';
window.__MUI_USE_NEXT_TYPOGRAPHY_VARIANTS__ = true;
const initialState = {};
const store = configureStore(initialState);
const auth = localStorage.jwtToken ? true : false;
// Check for token to keep user logged in
if (localStorage.jwtToken) {
// Set auth token header auth
const token = JSON.parse(localStorage.jwtToken);
setAuthToken(token);
// Decode token and get user info and exp
const decoded = jwtDecode(token);
// Set user and isAuthenticated
store.dispatch(setCurrentUser(decoded));
// Check for expired token
const currentTime = Date.now() / 1000; // to get in milliseconds
if (decoded.exp < currentTime) {
// Logout user
logoutUser();
// Redirect to login
window.location.href = './';
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
errors: {},
};
}
render() {
return (
<ThemeWrapper>
<AppContext.Consumer>
{changeMode => (
<Switch>
{/* <Route path="/" exact component={Login} /> */}
<PrivateRoute
path="/app"
auth={auth}
component={props => <Application {...props} changeMode={changeMode} />}
/>
<Route component={Auth} />
<Route component={NotFound} />
</Switch>
)}
</AppContext.Consumer>
</ThemeWrapper>
);
}
}
export default App;
infectreducer
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { ReactReduxContext } from 'react-redux';
import getInjectors from './reducerInjectors';
/**
* Dynamically injects a reducer
*
* #param {string} key A key of the reducer
* #param {function} reducer A reducer that will be injected
*
*/
export default ({ key, reducer }) => WrappedComponent => {
class ReducerInjector extends React.Component {
static WrappedComponent = WrappedComponent;
static contextType = ReactReduxContext;
static displayName = `withReducer(${WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'})`;
constructor(props, context) {
super(props, context);
getInjectors(context.store).injectReducer(key, reducer);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return hoistNonReactStatics(ReducerInjector, WrappedComponent);
};
const useInjectReducer = ({ key, reducer }) => {
const context = React.useContext(ReactReduxContext);
React.useEffect(() => {
getInjectors(context.store).injectReducer(key, reducer);
}, []);
};
export { useInjectReducer };
reducerinjector
import invariant from 'invariant';
import { isEmpty, isFunction, isString } from 'lodash';
import checkStore from './checkStore';
import createReducer from '../redux/reducers';
export function injectReducerFactory(store, isValid) {
return function injectReducer(key, reducer) {
if (!isValid) checkStore(store);
invariant(
isString(key) && !isEmpty(key) && isFunction(reducer),
'(app/utils...) injectReducer: Expected `reducer` to be a reducer function',
);
// Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
if (
Reflect.has(store.injectedReducers, key)
&& store.injectedReducers[key] === reducer
) return;
store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.injectedReducers));
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectReducer: injectReducerFactory(store, true),
};
}
Use persistedState. This is index.js file example
function saveToLocalStorage(state) {
try {
const serializedState = JSON.stringify(state)
localStorage.setItem('state', serializedState)
} catch (err) {
console.log(err)
}
}
function loadFromLocalStorage() {
try {
const serializedState = localStorage.getItem('state');
if (serializedState === null) return undefined;
return JSON.parse(serializedState)
} catch (err) {
console.log(err)
return undefined;
}
}
const persistedState = loadFromLocalStorage();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
persistedState,
applyMiddleware(logger, sagaMiddleware))
sagaMiddleware.run(watchLoadData);
store.subscribe(() => saveToLocalStorage(store.getState()))
ReactDOM.render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>,
document.getElementById('root')
);

MSAL authentication and authrization with React.js

I am fairly new to React and trying to implement Single Sign On Authentication in my React App.
Objectives:
Provide a login page where the user can enter their email address
On click of Sign-in user get the SSO popup (based Azure AD) to accept the terms and sign-in
Call graph API to retrieve user details (email ID, etc.)
Retrieve the sign in token and store in browser cache (localStorage) and use it for subsequent URL accesses (React routes).
I have come across MSAL (https://github.com/AzureAD/microsoft-authentication-library-for-js) which seems to be useful for this.
What I have tried:
Based on the MSDN docs: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-javascript-spa, I have registered my React SPA app in the Azure and got the client ID.
I have created a single js file (Auth.js) to handle sign-in, token generation and graph API call as mentioned in the docs: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-javascript-spa#use-the-microsoft-authentication-library-msal-to-sign-in-the-user
In my index.js I have configured the routes:
ReactDOM.render(<MuiThemeProvider theme={theme}>
<Router>
<Switch>
<Route path="/" exact component={Projects}/>
<Route path="/projects" exact component={Projects}/>
<Route path="/admin" exact component={Admin}/>
<Route path="/projectstages/:projectNumber" exact component={ProjectStages}/>
<Route path="/formspage" exact component={FormsPage}/>
<Route path="/users" exact component={UserManagement}/>
</Switch>
</Router>
</MuiThemeProvider>, document.getElementById('root'));
These routes (components) gets rendered within the main App.jsx component:
class App extends Component {
render(){
return(
<div className="App">
{this.props.children}
</div>
);
}
}
How do I integrate this within my React app so that only authenticated users can access the React routes along with the objectives I mentioned above? Please let me know if I can provide more details or explain more about this.
This is usually achieved using higher-order-components.
The idea is, when you load a page that requires authentication, you call an api to get authentication using access token stored from your cookies or whatever storage you use. Then you need to wrap your protected routes to a HOC that checks the authentication data.
import React, {useState, useContext, useRef, createContext } from 'react'
const AuthContext = createContext(null)
export const withAuth = (requireAuth = true) => (WrappedComponent) => {
function Auth(props) {
const isMounted = useRef(false);
// this is the authentication data i passed from parent component
// im just using
const { loading, error, auth } = useContext(AuthContext);
useEffect(() => {
isMounted.current = true;
}, []);
if (!isMounted.current && loading && requireAuth !== 'optional') {
return (<span>Loading...</span>);
}
if ((!auth || error) && requireAuth === true) {
return (<Redirect to="/login" />);
} if (auth && requireAuth === false) {
return (<Redirect to="/" />);
}
return (
<WrappedComponent {...props} />
);
}
return Auth;
};
export function AuthenticationProvider(props) {
const [auth, setAuth] = useState()
const [error, setErr] = usetState()
const [loading, setLoading] = useState(true)
useEffect(() => {
// get authentication here
api.call('/auth')
.then(data => {
setAuth(data)
setLoading(false)
})
.catch(err => {
setLoading(false)
setErr(err)
})
})
return (
<AuthContext.Provider value={{ auth, error, loading }}>
{children}
</AuthContext.Provider>
)
}
Then you can wrap your App with the Authentication Provider
<AuthenticationProvider>
<App/>
</AuthenticationProvider>
And for each of the pages, you use the HOC like this
function ProtectedPage(props){
// your code here
}
export default withAuth(true)(ProtectedPage)
I'd like to recommend to use package for this:
https://www.npmjs.com/package/react-microsoft-login
Install:
yarn add react-microsoft-login
# or
npm i react-microsoft-login
Import and configure component:
import React from "react";
import MicrosoftLogin from "react-microsoft-login";
export default props => {
const authHandler = (err, data) => {
console.log(err, data);
};
return (
<MicrosoftLogin clientId={YOUR_CLIENT_ID} authCallback={authHandler} />
);
};

React Router 4 and props.history.push

There's something driving me crazy in React, and I need your help. Basically, when the user clicks "My Profile", I want to redirect to the user's profile. To do that, I use the following snippet
viewProfile = () => {
console.log('My USER ID: ', this.props.userId);
this.props.history.push(`/profile/${this.props.userId}`);
}
This should work. However, although the URL is changing to the correct URL when 'My Profile' is clicked, the page isn't appearing. It just stays as the old page.
After googling for a while, I know it's something to do with redux ... However, I can't find a solution. (this.props.userId is coming from a Layout component, which is in turn getting it from redux store)
Here's my code:
// React
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
// Redux
import { connect } from 'react-redux';
import * as actions from '../../../store/actions/';
// Containers and Components
...// more imports
const styles = // styles
class NavBar extends Component {
handleAuthentication = () => {
if (this.props.isAuthenticated) {
this.props.history.push('/logout');
} else {
this.props.history.push('/login');
}
};
viewProfile = () => {
console.log('My USER ID: ', this.props.userId);
this.props.history.push(`/profile/${this.props.userId}`);
};
render() {
let buttons = (
<Auxillary>
{this.props.isAuthenticated ? (
<RaisedButton
label="MY PROFILE"
primary={true}
style={styles.button}
onClick={this.viewProfile} // THIS IS THE METHOD
/>
) : null}
<RaisedButton
backgroundColor={grey900}
label={this.props.isAuthenticated ? 'LOGOUT' : 'LOGIN'}
labelColor={grey50}
style={styles.button}
onClick={this.handleAuthentication}
/>
</Auxillary>
);
let itemSelectField = null;
if (this.props.location.pathname === '/items') {
itemSelectField = (
<ItemSelectField
onSelectTags={tags => this.props.filterItemsByTagName(tags)}
/>
);
}
let bar = null;
if (
this.props.location.pathname !== '/login' &&
this.props.location.pathname !== '/register'
) {
bar = (
<AppBar
style={styles.appBar}
title={itemSelectField}
iconElementLeft={
<img style={styles.logoHeight} src={Logo} alt="logo" />
}
iconElementRight={buttons}
/>
);
}
return <Auxillary>{bar}</Auxillary>;
}
}
const mapDispatchToProps = dispatch => {
return {
filterItemsByTagName: selectedTags =>
dispatch(actions.filterItemsByTagName(selectedTags))
};
};
export default withRouter(connect(null, mapDispatchToProps)(NavBar));
You can find the entire app here: https://github.com/Aseelaldallal/boomtown/tree/boomtown-backend-comm
I'm going crazy trying to fix this. help.
I think has to do with you using <BrowserRouter>. It creates its own history instance, and listens for changes on that. So a different instance will change the url but not update the <BrowserRouter>. Instead you can use ConnectedRouter and pass a history instance as
import createHistory from 'history/createBrowserHistory';
...
const history = createHistory();
...
<ConnectedRouter history={history}>
...
</ConnectedRouter>
You need to integrate the react-router-redux.
Its look like the updated state is not reflecting in the container as redux do shallow comparison to check whether the component need to be update or not.
import {Router} from 'react-router-dom';
import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
const history = createHistory()
const middleware = routerMiddleware(history)
const rootReducer = combineReducers({
router: routerReducer,
//other reducers
});
const store = createStore(rootReducer,applyMiddleware(middleware));
<Provider store={store}>
<ConnectedRouter>
//routes
</ConnectedRouter>
</Provider>
Also,at reducers,add this snippet.
let updatedStore = JSON.parse(JSON.stringify(state));

Categories