How to create a note taking app with React and localstorage — part 2
Hello guys.
I’m Nima HKH, a (Back && Front)*end Developer.
In the first part, we setup React.
Now it’s time to create our components. the first component has to be MainComponent that will includes all of the components. so inside of components
folder, create the MainComponent
folder and an index.js inside it.
index.js
import React from "react";
import NotesList from "../NotesList";
import NoteBooks from "../NoteBooks";
import CreateNote from "../CreateNote";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import { StateProvider, initialState, reducer } from "../../statemanagement";
import ModalBase from "../../Utils/Modal";
import ShowModal from "../../Utils/ShowModal";
import { useStyles } from "./styles";function MainComponent() {
const classes = useStyles();return (
<React.Fragment>
<Paper className={classes.root}>
<Grid container spacing={3}>
<StateProvider initialState={initialState} reducer={reducer}>
<React.Fragment>
<ModalBase />
<ShowModal />
<Grid item xs={2}>
<NoteBooks />
</Grid>
<Grid item xs={5}>
<NotesList />
</Grid>
<Grid item xs={5}>
<CreateNote />
</Grid>
</React.Fragment>
</StateProvider>
</Grid>
</Paper>
</React.Fragment>
);
}export default MainComponent;
Exactly, this component is just index of all components. what I used here?
React.Frgament: the Fragment is a React API to just wrap all components without DOM effects on UI. so we can wrap the components without using div DOM.
ModalBase and ShowModalBase: this two Modals must be the first level after body tag. I moved them inside of the MainComponent because Material-UI is doing this for all. so if we are not using Material-UI or another UI kits, we have to move Modals inside of the first level after body tag. this will help us to control the modal behavior on top of the all elements and this is a best practice.
useStyles: The useStyles is exported from the styles that is a hook that is inside of Material-ui. you can read about it here.
stateProvider: this is a provider to provide states in context API. I have a gist that you can read it here:
Inside of statemanagement
I wrote it.
styles : this is styles of the component that is inside of component root.
styles.js:
import { makeStyles } from "@material-ui/core/styles";export const useStyles = makeStyles(theme => ({
root: {
padding: theme.spacing(3, 2),
width: "90%",
margin: "0 auto",
marginTop: 100
}
}));
makeStyles: it is that hook that we talked about it .
Now we have to create NoteBooks, NotesList, and CreateNote
NoteBooks Component
Inside of this component we have two files. index.js and styles.js. of course index is about the main structure of the component and the styles
is styles of the component.
index.js
import React, { useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useStateValue } from "../../statemanagement";
import { useListStyles as useStyles } from "./styles";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemAvatar from "@material-ui/core/ListItemAvatar";
import ListItemText from "@material-ui/core/ListItemText";
import Avatar from "@material-ui/core/Avatar";
import FolderIcon from "@material-ui/icons/Folder";
import LocalStorage from "../../Utils/localStorage";function NoteBooks() {
const classes = useStyles();
const [activeNote, setActiveNote] = useState("all");
const [, dispatch] = useStateValue();/**
* show a list of notes of a notebook and dispatch it into Context
**/
function showNotesOf(Notebook) {
let NoteNextMonth = LocalStorage.getNotebooks("Next Month");
let University = LocalStorage.getNotebooks("University");
let Home = LocalStorage.getNotebooks("Home");
let Notes = LocalStorage.getNotebooks("notes");
setActiveNote(Notebook);
if (Notebook === "all") {
let All;
NoteNextMonth = NoteNextMonth !== null ? JSON.parse(NoteNextMonth) : [];
University = University !== null ? JSON.parse(University) : [];
Home = Home !== null ? JSON.parse(Home) : [];
Notes = Notes !== null ? JSON.parse(Notes) : [];
All = [...NoteNextMonth, ...University, ...Home, ...Notes];
if (All.length > 0) {
dispatch({ type: "newNote", notes: All });
}
} else {
let Notes;
if (Notebook === "Next Month") {
Notes = NoteNextMonth;
}
if (Notebook === "University") {
Notes = University;
}
if (Notebook === "Home") {
Notes = Home;
}Notes = Notes !== null ? JSON.parse(Notes) : [];
dispatch({ type: "newNote", notes: Notes });
}
}return (
<React.Fragment>
<Typography
variant="h5"
align="center"
color="primary"
gutterBottom
noWrap
>
Note Books
</Typography><div className={classes.noteBooksContainer}>
<div className={classes.demo}>
<List dense={false}>
<ListItem
className={[
classes.noteBookList,
activeNote === "all" && classes.active
].join(" ")}
onClick={() => showNotesOf("all")}
>
<ListItemAvatar>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="All" />
</ListItem><ListItem
className={[
classes.noteBookList,
activeNote === "Next Month" && classes.active
].join(" ")}
onClick={() => showNotesOf("Next Month")}
>
<ListItemAvatar>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Next Month" />
</ListItem><ListItem
className={[
classes.noteBookList,
activeNote === "University" && classes.active
].join(" ")}
onClick={() => showNotesOf("University")}
>
<ListItemAvatar>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="University" />
</ListItem><ListItem
className={[
classes.noteBookList,
activeNote === "Home" && classes.active
].join(" ")}
onClick={() => showNotesOf("Home")}
>
<ListItemAvatar>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Home" />
</ListItem>
</List>
</div>
</div>
</React.Fragment>
);
}export default NoteBooks;
It is not very to much complex. I think just we have to know what is dispatch. the other things are from the Marterial-UI’s list example.
dispatch : as it shows, this function is a dispatcher form React’s context API
that dispatches the states that consumes from the context API.
NoteLists Component
This component has three components inside. index.js
, Notes.js
, styles.js
.
The Notes.js
is a component to render each item from the list that will render.
index.js : because this file is too big, i put the github file’s link:
useStateValue : the useStateValue is a hook that is exported from the statemanagement folder and will dispatch and consume the states into context API.
searchFor function : this function is an helper to find an item by key in an array object. with this function we will search by categories and keywords inside of notes list.
search and searchCategory : these two functions are doing search by category or keywords with searchFor helper function . at the first, they fetch the obejcts, and then search inside of them.
handleCheckbox: this function is handling checkboxes to moving them into another notebook.
Notes.js component:
import React from "react";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import LocalStorage from "../../Utils/localStorage";
import { useStateValue } from "../../statemanagement";
import ButtonGroup from "@material-ui/core/ButtonGroup";
import Divider from "@material-ui/core/Divider";
import Grid from "@material-ui/core/Grid";
import { useNoteStyles as useStyles } from "./styles";
import Checkbox from "@material-ui/core/Checkbox";function Note(props) {
const { item, row } = props;
const { id, title, category } = item;
const [checkbox, setCheckbox] = React.useState(false);
const classes = useStyles();
const [, dispatch] = useStateValue();function handleChangeCheckBox() {
setCheckbox(!checkbox);
props.setCheckbox(!checkbox, id);
}function deleteNote() {
const NoteBookOfTheNote = item.notebook;
let getObjectsOfTheNoteBook = JSON.parse(
LocalStorage.getNotebooks(NoteBookOfTheNote)
);
if (getObjectsOfTheNoteBook === null) {
getObjectsOfTheNoteBook = JSON.parse(LocalStorage.getNotes());
}let removeNote = getObjectsOfTheNoteBook.filter(
note => note.id !== item.id
);
LocalStorage.rmNoteBook(
NoteBookOfTheNote === "" ? "notes" : NoteBookOfTheNote
);
LocalStorage.set(
NoteBookOfTheNote === "" ? "notes" : NoteBookOfTheNote,
JSON.stringify(removeNote)
);dispatch({ type: "newNote", notes: removeNote });
}function updateNote() {
dispatch({ type: "openModal", modal: true, edit: id });
}function showNote() {
dispatch({ type: "showMessage", showModal: true, show: id });
}return (
<Paper className={classes.paper}>
<Grid container>
<div className={title}>
<Checkbox
checked={checkbox}
onChange={handleChangeCheckBox}
value="checkedA"
inputProps={{
"aria-label": "primary checkbox"
}}
/>
{row + 1}- {title}({category})
</div>
</Grid>
<Divider variant="middle" />
<Grid container>
<ButtonGroup
color="primary"
aria-label="outlined primary button group"
className={classes.button}
>
<Button variant="outlined" color="secondary" onClick={deleteNote}>
Delete
</Button>
<Button variant="outlined" color="primary" onClick={updateNote}>
Update
</Button>
<Button variant="outlined" color="primary" onClick={showNote}>
Show
</Button>
</ButtonGroup>
</Grid>
</Paper>
);
}export default Note;
The component has three buttons. Delete, Show, Update. and all of them has a function.
Ok. the Localstorage objects is helping to much to make this three functions. the algorithme is very easy to delete.
The Show and Update are just dispatching states to show the modals.
CreateNote Component
This component has a Snackbar.js that will show some errors if user inssert duplicated item (the item that exists by ID) and an index.js as usual and also style.js.
The most important part is index.js
.
index.js : again it is a big component and i will just share the functions with you. you can see the full version, in my github.
...
function CreateNote() {
const classes = useStyles();
const [state, setState] = React.useState({
id: 0,
category: "",
notebook: "",
message: "",
title: ""
});
const inputLabel = React.useRef(null);
const [labelWidth, setLabelWidth] = React.useState(0);
const [openSnackbar, setOpenSnackbar] = React.useState(false);
const [, dispatch] = useStateValue();React.useEffect(() => {
setLabelWidth(inputLabel.current.offsetWidth);
}, []);/**
* handle change inputs
**/
function handleChange(name, event) {
setState({
...state,
[name]: event.target.value,
id: new Date().getTime()
});
}/**
* Add notes inside of localStorage and context api
**/
function addToNotes() {
//note book is not set, so set the Note in "Note" object
if (state.notebook === "") {
const allNodes = LocalStorage.getNotes();
let allNodesObject = allNodes !== null ? JSON.parse(allNodes) : [];
const rowExists = LocalStorage.rowExists(state);
if (rowExists.length === 0) {
setOpenSnackbar(false);
if (allNodesObject.length === 0) {
allNodesObject = [state];
} else {
allNodesObject.push(state);
}
LocalStorage.setNotes(JSON.stringify(allNodesObject));
dispatch({
type: "newNote",
notes: allNodesObject
});
} else {
setOpenSnackbar(true);
}
} else {
const allNodes = LocalStorage.getNotebooks(state.notebook);
let allNodesObject = allNodes !== null ? JSON.parse(allNodes) : [];
//set the note inside note book
const rowExists = LocalStorage.rowExists(state);
if (rowExists.length === 0) {
setOpenSnackbar(false);
if (allNodesObject.length === 0) {
allNodesObject = [state];
} else {
allNodesObject.push(state);
}
LocalStorage.set(state.notebook, JSON.stringify(allNodesObject));
dispatch({
type: "newNote",
notes: allNodesObject
});
} else {
setOpenSnackbar(true);
}
}
}/**
* On component Did mount , send data from localStorage into context api
**/
React.useEffect(() => {
let All;
let NoteNextMonth = LocalStorage.getNotebooks("Next Month");
let University = LocalStorage.getNotebooks("University");
let Home = LocalStorage.getNotebooks("Home");
let Notes = LocalStorage.getNotebooks("notes");NoteNextMonth = NoteNextMonth !== null ? JSON.parse(NoteNextMonth) : [];
University = University !== null ? JSON.parse(University) : [];
Home = Home !== null ? JSON.parse(Home) : [];
Notes = Notes !== null ? JSON.parse(Notes) : [];
All = [...NoteNextMonth, ...University, ...Home, ...Notes];
if (All.length > 0) {
dispatch({ type: "newNote", notes: All });
}
}, [dispatch]);
...
Multiobject state: here I create a multiobject state to handle form states easily without writing multiple functions for each input. of course it is a best practice to handle inputs with one useState object.
handleChange : this method is changing the input values with one name
and an event to get target value. here you will see the trick of spreas in ES6.
setState({
...state,
[name]: event.target.value,
id: new Date().getTime()
});
This line of code, will replace the value for the exact input name in the state object. the id key, is a timestamp to check the unique id for the record in localstorage and will compare before insert.
addToNotes : as it expects, this method just will insert the note into its notebook/notes. if the row is not exists ( the timestampt that we talk about it before ) so the note will insert into the selected caretgory and notebook.
useEffect : the useEffect is one of the React hooks that will perform side effects. in our component, this method will list all of the notes after dispatching in addToNotes
method is finished. then it will send it into NotesList component.
Ok. the MainComponent
is finished.
Starting the project :
After making the components (or cloning my repository) you will have to run the project with npm run start
on localhost:3000 and you will see that everything is work correctly.
The next steps
The next step is writing some tests on Localstorage functionalities and then deploy our project on surge.sh. also we will need some CI/CD by Travis, CircleCI and a simple DockerFile for our project.