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!
Snackbar
In the case of the Material UI component library they named this component Snackbar.
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)
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';
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 });
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
};
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;
}
};
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>
}
/>
);
};
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...
},
}));
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',
},
});
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....
}));
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';
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 });
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'
}
}
});
*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 });
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;
}
};
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;
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>
);
}
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 ',
})
);
};
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!
Top comments (0)