React – Saving an Encrypted List to Local Storage

Overview

Local Storage is a web storage object in the browser with a much greater capacity than cookies and twice that of Session Storage. Unlike Session Storage, the data never expires unless the user clears the browser’s cache. You can store up to 10mbs of data and it is accessible in any browser tab. Typically tokens, view preferences, shopping carts, wish lists, and data for offline usage are what usually gets stored. Unlike cookies, a site does not need a user’s consent, so it’s good idea to encrypt the data when possible.

The example below will demonstrate how a simple mock up search page that will store a search term history as a name value pair. For security as mentioned above, the data will be encrypted with AES encryption. For another example about encryption with crypto.js and React.JS check out this article on .

Storage Comparison Table

CookiesLocal StorageSession Storage
Capacity4kb x 2010mb5mb
AccessibilityAny WindowAny WindowSame tab
Storage LocationBrowser & ServerBrowserBrowser
ExpiresSet ManuallyNever – Must be
cleared.
Tab is Closed
Sent with requestYesNoNo

Download the Source Files

GitHub Repo

Prerequisites

Crypto – For Data Encryption

This is just one of many options at your disposal to secure data in a cookie or local storage.

Install the library with npm. You can get the full documentation for Crypto.Js here.

npm install crypto.js

Material UIVersion 5

This example uses React-Material for the inputs and layout.

Note: Version 5 was released on Sep 16, 2021.

Current version documentation click here

npm install @mui/material @emotion/react @emotion/styled

Crypto Helper for Data Encyption

These files will have the function for storing and securing the search phrases entered by a user,

Create/view 2 files under “.\src\global.

  • Local-Storage-Helper.js
  • Crypto-Helper.js

Crypto-Helper

The helper file uses AES to encrypt and decrypt a string. The Private Key is static but can use a dynamic variable such as date or a random key for a session id, etc. If you using something that can change during the session then you may want to consider checking the decrypted value twice just in case the data is not readable.,

Source code for “.\global\Crypto-Helper.js

var CryptoJS = require("crypto-js");

export function AESEncrypt(pureText) {    
    const privateKey=`secret key 123`;    
    var ciphertext = encodeURIComponent(CryptoJS.AES.encrypt(JSON.stringify(pureText), privateKey).toString());    
    return ciphertext;
}

export function AESDecrypt(encryptedText) {  
    const privateKey=`secret key 123`;    
    var bytes  = CryptoJS.AES.decrypt(decodeURIComponent(encryptedText), privateKey);
    var decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));    
    return decryptedData;
}

How Entries are Stored in the List

The store will save the new name value pair in Local Storage.
The function does the following before added it to the list
1. Checks if the searchHist key exists and creates it if it doesn’t.
2. If it the key does exist then it will decrypt the list.
3. Loop through the current list and make sure they meet the following requirements before adding them into the list:
3a. If there the entry exceeds that max number of days old
3b. If the entry is unique – Do not add a duplicate entry

The LocalStoreList() function also encrypts the array of name value pairs before saving it.

Local Storage Functions

Below is the source code for .\global\Local-Storage-Helper.js. The insert, delete, get operations in Local Storage using the functions labeled LocalStoreList(), DeleteLocalStoreListItem(), and GetStoreList().

import { AESEncrypt, AESDecrypt } from 'global/Crypto-Helper';
import { differenceInDays } from 'date-fns';

export function LocalStoreList(localSessionName, value, maxDaysOld) {    
    var localStorageData = localStorage.getItem(localSessionName);        
    var today = new Date();
    
    const cDate = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate();
    
    if(localStorageData == null) {        
        var data = [{value: value, date: cDate}]
        var dataArr = [];
        dataArr.push(data);
        localStorageData = localStorage.setItem(localSessionName,  AESEncrypt(data));             
    }
    else {           
        
        var newDataArr = [];
        let dataArr = AESDecrypt(localStorage.getItem(localSessionName));      
        var isDuplicate= false;
        dataArr.forEach(element => {            
            let recordDate = new Date(element.date);           
            var daysDiff = differenceInDays(today, recordDate,  { addSuffix: false })
    
                if(daysDiff <= maxDaysOld) {
                    newDataArr.push(element);    
                    
                    // Check for duplicate entry
                    if(element.value.toLowerCase() === value.toLowerCase()) {
                        isDuplicate = true;            
                    }                                    
                }                
        });      

        // Add new value if value is not a duplicate
        if(!isDuplicate) {
           newDataArr.push({value: value, date: cDate});        
        }
        
        localStorage.setItem(localSessionName, AESEncrypt(newDataArr));                                                                  
        return newDataArr;  
    }
}

export function DeleteLocalStoreListItem(localSessionName, deleteValue) {    
    var localStorageData = localStorage.getItem(localSessionName);        
    
    if(localStorageData !== null) {        
        
        var newDataArr = [];
        let dataArr = AESDecrypt(localStorage.getItem(localSessionName));      
        
        dataArr.forEach(element => {                                                    
            if(element.value !== deleteValue) {
                newDataArr.push(element);    
            }                        
        });        
                
        localStorage.setItem(localSessionName, AESEncrypt(newDataArr));                                                      
            
        return newDataArr;  
    }
}

export function GetLocalStorageData(localSessionName) {    
    var data = localStorage.getItem(localSessionName);        
    let array = [];
    
    if(data == null) {
        return array;
    }
    else {   
        const lastSearch = AESDecrypt(data);                 
        return lastSearch;  
    }
}

Search Form Component

The “.\components\search\SearchForm.js” renders new items added to the search form. When a new item is listed the getCookieData() function is called. The <TextField /> is set up to handle a enter key for submission along with the <Button/>. The field is cleared out after each entry which may not be ideal for a real search page but it is there for faster search term entry.

import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';

export default function SearchForm(props) {
  return (
    <Box
      component="form"
      sx={{
        '& .MuiTextField-root': { m: 1, width: '65ch' },
      }}            
      autoComplete="off"
    >
      <div>
        <TextField          
          fullWidth
          size="small"
          name="searchStr"
          id="searchStr"
          label="Search"
          onChange={(e) => props.handleChangeSearchTerm(e)}
          onKeyDown={(e) => props.handleKeyPress(e)}
          defaultValue=""
          value={props.searchTerm}
          InputLabelProps={{ shrink: true }} 
          placeholder="What?"
        />  
        
        <Button variant="outlined" 
                onClick={(e) => props.handleSubmit(e)}
                color="primary" 
                style={{marginLeft: '10px', marginTop: '10px'}}
          >
          Search
        </Button>
      </div>            
    </Box>
  );
}

Parent Page

In .\pages\index.js passes the event handler for new items added to the search form. When a new item is listed the GetLocalStorageData() function is called.

The Parent Page ‘./pages/search/index.js. This component handles the following:

  • handleChangeToList() – Sets the searchHistory variable with GetLocalStorageData() from “./global/Local-Storage-Helper“.
  • handleChangeSearchTerm() – – Sets the searchHistory value.
  • handleSubmit() – Adds the new value to the search history with GetLocalStorageData() from “./global/Local-Storage-Helper“.
  • handleDelete()– Removes a value from the search history with DeleteLocalStoreListItem() from “./global/Local-Storage-Helper“.
  • handleKeyPress() – Submits the form or adds the value to the list when the enter key is pressed.

Code for ‘.\pages\search\index.js‘.

import React, { useState } from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import SearchForm from 'components/search/SearchForm';
import styles from 'styles/Search.module.css';
import { LocalStoreList, DeleteLocalStoreListItem, GetLocalStorageData } from 'global/Local-Storage-Helper';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
import Link from '@mui/material/Link';

export default function Search() {
    
    const [searchHistory, setSearchHistory] = useState(GetLocalStorageData('SearchHist'));
    const [searchTerm, setSearchTerm] = useState('');
    const maxDaysOld = 3;
    
    const handleChangeToList = (event) => {       
        setSearchHistory(GetLocalStorageData('SearchHist'));
    }
        
    const handleChangeSearchTerm = (event) => {    
        setSearchTerm(event.target.value);    
    }
    
    const handleSubmit = (event) => {                
        LocalStoreList('SearchHist', searchTerm, maxDaysOld);
        handleChangeToList();
        setSearchTerm('');
    }

    const handleClickOnLink = (val) => {               
        setSearchTerm(val);
    }
        
    const handleDelete = (val) => {               
        DeleteLocalStoreListItem('SearchHist', val);
        handleChangeToList();
    }

    const handleKeyPress = (event) => {
        if(event.keyCode === 13) {            
            LocalStoreList('SearchHist', searchTerm, maxDaysOld);
            handleChangeToList();
            setSearchTerm('');                
         }
    }
        
    return (
        <div className={styles.searchForm}>
            <Box sx={{ flexGrow: 1 }}>
                <Grid container spacing={2}>
                    <Grid item xs={12} lg={3} xl={2}> </Grid>
                    <Grid item xs={12} lg={9} xl={10}>
                        <Grid container spacing={2}>
                        
                            <Grid item xs={10} lg={10} xl={10}>
                               <SearchForm 
                                           handleChangeSearchTerm={handleChangeSearchTerm} 
                                           handleSubmit={handleSubmit}
                                           handleKeyPress={handleKeyPress}
                                           searchTerm={searchTerm}/>
                            </Grid>
                            <Grid item xs={10} lg={10} xl={10}>
                                <ul>
                                {searchHistory.map((element) =>                                   
                                <li>
                                    <Link href="#" underline="none"  onClick={(e) => handleClickOnLink(element.value)}>{element.value} - {element.date}</Link> 
                                    <IconButton aria-label="delete" size="small" color="secondary" onClick={(e) => handleDelete(element.value)}>
                                        <DeleteIcon fontSize="inherit" />
                                    </IconButton>
                                </li>
                                )}  
                                </ul>                                                                  
                            </Grid>
                        </Grid>    
                    </Grid>
                </Grid>
            </Box>
        </div>
    )
}
Photo by Jill Wellington from Pexels