skip to Main Content

I’m working on a basic journaling app based using some inspiration from Journalisticapp.com. I’m using this as a way to learn/practice React and React Router.

The Entries component should contain a list of journal entries that are pulled with a loader function on the /journal index route. Each entry is added via React Router form element, then submitted to /entry/add with an action function.

After submitting the form I want the journal entries to update, but it requires a full page reload. How can I get only the Entries component to reload after submitting the form without having to reload the page?

Routes.jsx

import React from 'react';
import { createRoot } from 'react-dom/client'
import { RouterProvider, createBrowserRouter, createRoutesFromElements, Route, Routes } from "react-router-dom";
import localforage from 'localforage';

import RootLayout from './layouts/RootLayout'
import JournalLayout from './layouts/JournalLayout';
import Entries from './components/Entries';

const journalDB = localforage.createInstance({
    driver: localforage.INDEXEDDB,
    storeName: 'journalDB',
})

const EntryList = await journalDB.getItem('entries') || []

const addEntry = (entry = { id, content }) => {
    journalDB.setItem('entries', [...EntryList, entry]).then((entries) => {
        console.log(`entry ${entry.id} saved`)
    }).catch((err) => console.log(err))
}

const removeEntry = (oldEntry = {}) => {
    let filteredEntries = Entries.filter(entry => entry !== oldEntry)

    journalDB.setItem('entries', filteredEntries).then((entries) => {
        console.log(`entry ${oldEntry.id} removed`)
    }).catch((err) => console.log(err))
}

const editEntry = (originEntry = { id }, update = { content }) => {
    let newEntry = { ...originEntry, ...update }
    removeEntry(originEntry)
    addEntry(newEntry)
}

const router = createBrowserRouter(
    createRoutesFromElements(
        <Route
            path='/'
            element={<RootLayout />}>
            <Route
                id='journal'
                path='journal'
                element={<JournalLayout />}>
                <Route
                    index
                    id='entries'
                    element={<Entries />}
                    loader={async () => { return EntryList }} />
                <Route
                    path='entry/add'
                    action={async ({ params, request }) => {
                        const req = await request.formData()

                        let entry = {
                            id: EntryList.length + 1,
                            content: req.get('content')
                        }

                        addEntry(entry)

                        return EntryList
                    }} />
            </Route>
        </Route>
    )
)

createRoot(document.querySelector('#root')).render(
    <RouterProvider router={router} />
);

JournalLayout.jsx

import React, { useEffect } from 'react'
import { Outlet, Form, useFetcher } from 'react-router-dom'

const JournalLayout = () => {
    const fetcher = useFetcher()
    return (
        <div>
            <div className="journal">
                <div>Journal</div>

                <Outlet />

                <div>
                    <fetcher.Form method='put' action='entry/add' >
                        <textarea rows={10} cols={30} type='text' name='content' />
                        <input type="submit" />
                    </fetcher.Form>
                </div>
            </div>
        </div >
    )
}

export default JournalLayout

Entries.jsx

import React, { useState, useEffect } from 'react'
import style from './Entries.scss'
import { useLoaderData } from 'react-router-dom'

const Entries = () => {
    const EntryList = useLoaderData()

    return (
        <div className={style.class}>{EntryList.map(({ id, content }, idx) => <div key={idx}>{id}: {content}</div>)}</div>
    )
}

export default Entries

2

Answers


  1. Why your problem is happening

    After looking over your code a few times, the problem lies in how you initialize the EntryList array. You make one call to the database to fetch results and nothing new ever happens to the array. The route also has the loader parameter passing back the array and not the results of a new call to the database.

    Thankfully this is a very easy fix, and it can be done in multiple ways.


    First option (off the top of my head)

    Instead of passing the EntryList array in the route’s loader, pass the results of a new call to the database (the call should fetch all the entries).

    <Route
      index
      id="entries"
      element={<Entries />}
      loader={async () => {
        try {
          const newEntries = await journalDB.getItem("entries");
          return newEntries;
        } catch (error) {
          console.error(error);
          // Pass original array when failure occurs.
          return EntryList;
        }
      }}
    />;
    

    Problems here is performance. Could be slow, depending on db and how the fetching function is implemented.


    Second option (more thought out, less calls to the database, so faster UI)

    When adding new entries, make sure you keep the EntryList up-to-date. This may be a solution you can use without needing to add some React Context or extra react hook callback code.

    If you choose to update the EntryList array every time you ADD/DELETE/UPDATE, you need to add the code to all functions that preform crud operations to the database. addEntry, editEntry, and removeEntry need to be updated and tested.

    See the code snippet below:

    let EntryList = (await journalDB.getItem("entries")) || [];
    
    const addEntry = (entry = { id, content }) => {
      journalDB
        .setItem("entries", [...EntryList, entry])
        .then((entries) => {
          // Log to user that it saved
          console.log(`entry ${entry.id} saved`);
          // Rewrite the `EntryList` array with new changes
          EntryList = entries;
        })
        .catch((err) => console.log(err));
    };
    

    See your full file with the (some) changes. I also have prettier format all my files, so I prettified your react code. Sorry!

    import React from "react";
    import { createRoot } from "react-dom/client";
    import {
      RouterProvider,
      createBrowserRouter,
      createRoutesFromElements,
      Route,
      Routes,
    } from "react-router-dom";
    import localforage from "localforage";
    
    import RootLayout from "./layouts/RootLayout";
    import JournalLayout from "./layouts/JournalLayout";
    import Entries from "./components/Entries";
    
    const journalDB = localforage.createInstance({
      driver: localforage.INDEXEDDB,
      storeName: "journalDB",
    });
    
    // -- Changes I made -----------------------
    let EntryList = (await journalDB.getItem("entries")) || [];
    
    const addEntry = (entry = { id, content }) => {
      journalDB
        .setItem("entries", [...EntryList, entry])
        .then((entries) => {
          // Log to user that it saved
          console.log(`entry ${entry.id} saved`);
          EntryList = entries;
        })
        .catch((err) => console.log(err));
    };
    // -----------------------------------------
    
    const removeEntry = (oldEntry = {}) => {
      let filteredEntries = Entries.filter((entry) => entry !== oldEntry);
    
      journalDB
        .setItem("entries", filteredEntries)
        .then((entries) => {
          console.log(`entry ${oldEntry.id} removed`);
        })
        .catch((err) => console.log(err));
    };
    
    const editEntry = (originEntry = { id }, update = { content }) => {
      let newEntry = { ...originEntry, ...update };
      removeEntry(originEntry);
      addEntry(newEntry);
    };
    
    const router = createBrowserRouter(
      createRoutesFromElements(
        <Route path="/" element={<RootLayout />}>
          <Route id="journal" path="journal" element={<JournalLayout />}>
            <Route
              index
              id="entries"
              element={<Entries />}
              loader={async () => {
                return EntryList;
              }}
            />
            <Route
              path="entry/add"
              action={async ({ params, request }) => {
                const req = await request.formData();
    
                let entry = {
                  id: EntryList.length + 1,
                  content: req.get("content"),
                };
    
                addEntry(entry);
    
                return EntryList;
              }}
            />
          </Route>
        </Route>
      )
    );
    
    createRoot(document.querySelector("#root")).render(
      <RouterProvider router={router} />
    );
    
    Login or Signup to reply.
  2. From what I can tell it seems that EntryList is computed only once when the app is loaded and the Routes.jsx file is processed. The loader function is simply returning the initially computed value.

    // Computes entry list when processing file
    const EntryList = await journalDB.getItem('entries') || [];
    
    ...
    
    const router = createBrowserRouter(
      createRoutesFromElements(
        ...
        <Route
          index
          id='entries'
          element={<Entries />}
          loader={async () => {
            // Returns computed value
            return EntryList;
          }}
        />
        ...
      )
    );
    

    The loader should call the DB/backend at runtime.

    const entryListLoader = async () => {
      // Computes entry list when loader called
      return await journalDB.getItem('entries') || [];
    };
    
    ...
    
    const router = createBrowserRouter(
      createRoutesFromElements(
        ...
        <Route
          index
          id='entries'
          element={<Entries />}
          loader={entryListLoader}
        />
        ...
      )
    );
    

    Edit how-to-trigger-loader-function-after-form-is-submitted

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search