DEV Community

loading...
Cover image for Let's Make a Redux + Material UI + OAuth Starter template! Pt 3. Alerts & Themes

Let's Make a Redux + Material UI + OAuth Starter template! Pt 3. Alerts & Themes

ethanny2 profile image Ethan Soo Hon ・6 min read

In this part of the series we will examine how to build an alert and theme system controlled by the redux store that you can use across your entire app. Although our buttons do change (due to conditional rendering) if someone interacts with your site it's always a good idea to let the user know if their actions went through successfully or failed!

Different types of alerts
Different libraries have different names for them but they serve the same purpose

Snackbar

In the case of the Material UI component library they named this component Snackbar.
Docs

All of the conditions listed in this description are achievable by passing in some props to the Snackbar component. Typically Snackbars are displayed on the bottom of the screen but through you can modify the position with the anchorOrigin prop. See the Full API here.

API

Looking at the API we can see that at the bare minimum we need to pass as props are the following...

open: bool //If true, Snackbar is open.
message: string 
// To put snackbar at top center
anchorOrigin: { vertical: 'top', horizontal: 'center' }
onClose: function 
//Calls our onClose after time is up
autoHideDuration: number (ms) 
Enter fullscreen mode Exit fullscreen mode

Because these props are the things that will customize any SnackBar/Alert it makes sense to set up our initial state (in our yet to be made reducer) as an object with the above key value pairs so we can easily spread the state object into the component.

Time to Code

Similarly we will begin setting up the our redux code to handle this Alert system.

1) Types:
Simple enough, we have one to set/show a message and one to clear/hide a message.

export const SHOW_ALERT = 'SHOW_ALERT';
export const CLEAR_ALERT = 'CLEAR_ALERT';
Enter fullscreen mode Exit fullscreen mode

2) Action Creators:
showAlert returns an action object with a single key-value pair in its payload object; message.
clearAlert just returns an action object with the type since we use our INITIAL_STATE object in the reducer file to reset it back to normal

export const showAlert = (
    msgConfig = { message: 'default'}
) => ({ type: SHOW_ALERT, payload: { ...msgConfig } });

export const clearAlert = () => ({ type: CLEAR_ALERT });
Enter fullscreen mode Exit fullscreen mode

3) Reducers:
Here's how we set up our INITIAL_STATE object with key-value pairs matching the props that will go into the Snackbar component.

const INITIAL_STATE = {
    open: false,
    message: '',
    anchorOrigin: { vertical: 'top', horizontal: 'center' },
    autoHideDuration: 3500
};
Enter fullscreen mode Exit fullscreen mode

In the actual code handling SHOW_ALERT we just spread the previous state object (to keep all the other properties), set open to true and spread the action.payload into the object as well to get the message property.

const alertReducer = (state = INITIAL_STATE, action) => {
    switch (action.type) {
        case SHOW_ALERT:
            /*
       action.payload looks like
        {message:''}
      */
            return { ...state, open: true, ...action.payload };
        case CLEAR_ALERT:
            return { ...INITIAL_STATE };
        default:
            return state;
    }
};
Enter fullscreen mode Exit fullscreen mode

4) Components:
I will name this component Alert.js but in the returned JSX we will utilize the Snackbar component.

Note: Confusingly enough there is also a component in Material UI called Alert which we will not use

import { Snackbar, IconButton } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import { useDispatch, useSelector } from 'react-redux';
import { clearAlert } from '../actions/alertActions';

const Alert = () => {
    const alert = useSelector((state) => state.alert);
    const dispatch = useDispatch();
    const handleClose = () => dispatch(clearAlert());
    return (
        <Snackbar
            {...alert}
            onClose={handleClose}
            action={
                    <IconButton
                        size='small'
                        aria-label='close'
                        color='inherit'
                        onClick={handleClose}
                    >
                        <CloseIcon fontSize='small' />
                    </IconButton>
            }
        />
    );
};
Enter fullscreen mode Exit fullscreen mode

We use the useSelector hook to get the alert object out of state, we use useDispatch to grab the dispatch function then we spread all the properties from the alert object from state into the Snackbar. The action prop takes in some Components/JSX you can use to make the close button.

Theming

In addition to the makeStyles() hook we saw in part 2 Material UI also has a robust and customizable theme system that works by having a MuiThemeProvider component wrapped around your root component. Whatever created theme you passed in as a prop to that provider will be used whenever a makeStyles() hook is invoked

/*
This theme variable is usually the
default Material UI theme but if
it detects a theme provider component
wrapped around it, that theme will instead be used
*/
const useStyles = makeStyles((theme) => ({
    center: {
        etc...
                etc...
    },
}));
Enter fullscreen mode Exit fullscreen mode
Theming detail page
How would we switch to dark mode?


To create your own theme you need to leverage their API and use the createMuiTheme function. It takes in an object with key value pairs that can be set to colors (palette) font-sizes (via typography) and many more!

(*Note: I encourage everyone to look into the default theme object to see what can be set. It looks intimidating at first but it is just a giant object)

Dark Mode

This is such a common use case they have a whole section in the documentation devoted to this! In this case we just need to create a new theme and set the value of palette.type to 'dark' or 'light'.

const darkTheme = createMuiTheme({
  palette: {
    type: 'dark',
  },
});
Enter fullscreen mode Exit fullscreen mode
Dark mode differences
By default only these properties are changed when the mode is switched


Unfortunately this only modifies certain properties of the theme NOT including the primary or secondary colors. If you recall from article 2 we styled the button in our login component like so...

const useStyles = makeStyles((theme) => ({
    button: {
       etc...
       backgroundColor: theme.palette.primary.main,
         etc....
}));

Enter fullscreen mode Exit fullscreen mode

Thus switching the type to 'dark' will not affect theme.palette.primary so the button will remain the same color. If you wanted your component to be darkened as well we'll have to set our own palette.primary color when creating our theme!

Time to Code

For the sake of simplicity I will only have 2 themes to switch between; light and dark.

1) Types

export const TOGGLE_THEME = 'TOGGLE_THEME';

Enter fullscreen mode Exit fullscreen mode

2) Action Creators
That's it! The defined light and dark mode objects/themes are pre-defined in the themeReducer file

export const toggleTheme = () => ({ type: TOGGLE_THEME });
Enter fullscreen mode Exit fullscreen mode

3) Reducer
Since we are managing the theme object directly through redux our state will just be whatever object is the result of us calling the createMuiTheme() function. We create two themes for light and dark mode with the only difference being the primary.main color.

let INITIAL_STATE = {};
const LIGHT_MODE_STATE = createMuiTheme({
    palette: {
        type: 'light',
        primary: {
            main: '#3f51b5', 
            contrastText: '#fff'
        }
    }
});
const DARK_MODE_STATE = createMuiTheme({
    palette: {
        type: 'dark',
        primary: {
            main: '#000',
            contrastText: '#fff'
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

*Note: Any properties you don't set are inherited from the default theme so you can still use variables like typography, spacing etc... even though we didn't explicitly define it

We throw in a one-liner to detect the users theme preference from their computer via a function on the global window object.

let matched = window.matchMedia('(prefers-color-scheme: dark)').matches;
matched
    ? (INITIAL_STATE = { ...DARK_MODE_STATE })
    : (INITIAL_STATE = { ...LIGHT_MODE_STATE });
Enter fullscreen mode Exit fullscreen mode

Finally we write the reducer itself; very simple, we just toggle from light to dark.

const themeReducer = (state = INITIAL_STATE, action) => {
    switch (action.type) {
        case TOGGLE_THEME:
            //There is no payload we just replace the theme obj/state with the
            //opposite of whatever type is
            return state.palette.type === 'light'
                ? { ...DARK_MODE_STATE }
                : { ...LIGHT_MODE_STATE };
        default:
            return state;
    }
};
Enter fullscreen mode Exit fullscreen mode

4) Provider and wrapping up
Okay we have a theme, we have a light and dark mode in our redux store, now what? Now we need to funnel that theme object into the MuiThemeProvider component Material UI gives us. When the theme changes in the store it will be updated here as well. We take in children as props (using destructuring) so anything wrapped in this Provider still shows up on the screen.

import { MuiThemeProvider } from '@material-ui/core/styles';
import { useSelector } from 'react-redux';

function Theme({ children }) {
    const theme = useSelector((state) => state.theme);
    return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>;
}

export default Theme;
Enter fullscreen mode Exit fullscreen mode

Now we can wrap the theme provider at our root component (App.js or something). Also note we added our Alert component here so it always shows up if we trigger it.

import { makeStyles } from '@material-ui/core/styles';
import { useSelector } from 'react-redux';
import Alert from './Alert';
import Login from './Login';
import Logout from './Logout';
import ThemeProvider from './ThemeProvider';
import CssBaseline from '@material-ui/core/CssBaseline';

function App() {
    const classes = useStyles();
    const auth = useSelector((state) => state.auth);
    return (
        <ThemeProvider>
            <CssBaseline />
            <main >
                <Alert />
                {auth.loggedIn ? <Logout /> : <Login />}
            </main>
        </ThemeProvider>
    );
}
Enter fullscreen mode Exit fullscreen mode

Here we use a component called CSSBaseline also from Material UI (which they recommend to place at the root of your project) and it functions identically to Normalize CSS
(Provides good defaults, consistent styling, box-sizing:border-box and most importantly allows our theme switch from light to dark to also change the body background )

Let's test the Alert!

We setup both the Alert system and the theme system through redux but we never actually dispatched any actions to use them. For the theme we will make a switch in the next article, but you can switch between the "LIGHT_MODE_STATE" and "DARK_MODE_STATE" objects in the themeReducer to see how it would look. We want to see the alerts when a login succeeds, a login fails, a logout succeeds and a logout fails. All we have to do is dispatch our Alert action creator at the right time.

//Inside Login.js and Logout.js
    const onSuccess = (res) => {
        dispatch(googleOAuthLogin(res));
        dispatch(
            showAlert({
                message: 'Successfully logged in',
            })
        );
    };
    const onFailure = (res) => {
        dispatch(googleOAuthLogin(res));
        dispatch(
            showAlert({
                message: 'Login failed ',
            })
        );
    };
Enter fullscreen mode Exit fullscreen mode

Yes, the button should do nothing until you plug in your client id from the first article


We're done setting up redux! In the last article we will make the mobile responsive navbar that displays the user's info when they're logged in and a we'll make a switch for dark mode!

Discussion (0)

pic
Editor guide