Neil Girardi
Neil Girardi

Reputation: 4913

How can I change my data fetching strategy to avoid stale data on initial dynamic route renders?

I'm building a Next.js app which allows users to create and view multiple Kanban boards. There are a couple of different ways that a user can view their different boards:

  1. On the Home page of the app, users see a list of boards that they can click on.
  2. The main navigation menu has a list of boards users can click on.

Both use Next.js Link components.

Clicking the links loads the following dynamic page: src/pages/board/[boardId].js The [boardId].js page fetches the board data using getServerSideProps(). An effect fires on route changes, which updates the redux store. Finally, the Board component uses a useSelector() hook to pull the data out of Redux and render it.

The problem I'm experiencing is that if I click back and forth between different boards, I see a brief flash of the previous board's data before the current board data loads. I am hoping someone can suggest a change I could make to my approach to alleviate this issue.

Source code:

// src/pages/board/[boardId].js
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Board from 'Components/Board/Board'
import { useRouter } from 'next/router'
import { hydrateTasks } from 'Redux/Reducers/TaskSlice'
import { unstable_getServerSession } from 'next-auth/next'
import { authOptions } from 'pages/api/auth/[...nextauth]'
import prisma from 'Utilities/PrismaClient'

const BoardPage = ({ board, tasks }) => {
  const router = useRouter()
  const dispatch = useDispatch()

  useEffect(() => {
      dispatch(hydrateTasks({ board, tasks }))
  }, [router])

  return (
    <Board />
  )
}

export async function getServerSideProps ({ query, req, res }) {
  const session = await unstable_getServerSession(req, res, authOptions)
  if (!session) {
    return {
      redirect: {
        destination: '/signin',
        permanent: false,
      },
    }
  }
  const { boardId } = query

  const boardQuery = prisma.board.findUnique({
    where: {
      id: boardId
    },
    select: {
      name: true,
      description: true,
      id: true,
      TO_DO: true,
      IN_PROGRESS: true,
      DONE: true
    }
  })

  const taskQuery = prisma.task.findMany({
    where: {
      board: boardId
    },
    select: {
      id: true,
      title: true,
      description: true,
      status: true,
      board: true
    }
  })
  try {
    const [board, tasks] = await prisma.$transaction([boardQuery, taskQuery])
    return { props: { board, tasks } }
  } catch (error) {
    console.log(error)
    return { props: { board: {}, tasks: [] } }
  }
}

export default BoardPage

// src/Components/Board/Board.js
import { useEffect } from 'react'
import { useStyletron } from 'baseui'
import Column from 'Components/Column/Column'
import ErrorBoundary from 'Components/ErrorBoundary/ErrorBoundary'
import useExpiringBoolean from 'Hooks/useExpiringBoolean'
import { DragDropContext } from 'react-beautiful-dnd'
import Confetti from 'react-confetti'
import { useDispatch, useSelector } from 'react-redux'
import useWindowSize from 'react-use/lib/useWindowSize'
import { moveTask } from 'Redux/Reducers/TaskSlice'
import { handleDragEnd } from './BoardUtilities'
import { StyledBoardMain } from './style'

const Board = () => {
  const [css, theme] = useStyletron()
  const dispatch = useDispatch()

  useEffect(() => {
    document.querySelector('body').style.background = theme.colors.backgroundPrimary
  }, [theme])

  // get data from Redux
  const { boardDescription, boardName, columnOrder, columns, tasks } = useSelector(state => state?.task)

  // set up a boolean and a trigger to control "done"" animation
  const { boolean: showDone, useTrigger: doneUseTrigger } = useExpiringBoolean({ defaultState: false })

  const doneTrigger = doneUseTrigger({ duration: 4000 })

  // get width and height for confetti animation
  const { width, height } = useWindowSize()

  // curry the drag end handler for the drag and drop UI
  const curriedDragEnd = handleDragEnd({ dispatch, action: moveTask, handleOnDone: doneTrigger })

  return (
    <ErrorBoundary>
      <DragDropContext onDragEnd={curriedDragEnd}>
        <div className={css({
          marginLeft: '46px',
          marginTop: '16px',
          fontFamily: 'Roboto',
          display: 'flex',
          alignItems: 'baseline'
        })}>
          <h1 className={css({ fontSize: '22px', color: theme.colors.primary })}>{boardName}</h1>
          {boardDescription &&
            <p className={css({ marginLeft: '10px', color: theme.colors.primary })}>{boardDescription}</p>
          }
        </div>
        <StyledBoardMain>
          {columnOrder.map(columnKey => {
            const column = columns[columnKey]
            const tasksArray = column.taskIds.map(taskId => tasks[taskId])
            return (
              <Column
                column={columnKey}
                key={`COLUMN_${columnKey}`}
                tasks={tasksArray}
                title={column.title}
                status={column.status}
              />
            )
          })}
        </StyledBoardMain>
      </DragDropContext>

      {showDone && <Confetti
        width={width}
        height={height}
      />}

    </ErrorBoundary>
  )
}

export default Board

// src/pages/index.tsx
import React, {PropsWithChildren} from 'react'
import {useSelector} from "react-redux";
import {authOptions} from 'pages/api/auth/[...nextauth]'
import {unstable_getServerSession} from "next-auth/next"
import CreateBoardModal from 'Components/Modals/CreateBoard/CreateBoard'
import Link from 'next/link'
import {useStyletron} from "baseui";

const Index: React.FC = (props: PropsWithChildren<any>) => {
    const {board: boards} = useSelector(state => state)
    const [css, theme] = useStyletron()
    return boards ? (
        <>
            <div style={{marginLeft: '46px', fontFamily: 'Roboto', width: '600px'}}>
                <h1 className={css({fontSize: '22px'})}>Boards</h1>
                {boards.map(({name, description, id}) => (
                    <Link href="/board/[boardId]" as={`/board/${id}`} key={id}>
                        <div className={css({
                            padding: '20px',
                            marginBottom: '20px',
                            borderRadius: '6px',
                            background: theme.colors.postItYellow,
                            cursor: 'pointer'
                        })}>
                            <h2 className={css({fontSize: '20px'})}>
                                <a className={css({color: theme.colors.primary, width: '100%', display: 'block'})}>
                                    {name}
                                </a>
                            </h2>
                            <p>{description}</p>
                        </div>
                    </Link>
                ))}
            </div>
        </>
    ) : (
        <>
            <h1>Let's get started</h1>
            <button>Create a board</button>
        </>
    )
}

export async function getServerSideProps(context) {
    const session = await unstable_getServerSession(context.req, context.res, authOptions)
    if (!session) {
        return {
            redirect: {
                destination: '/signin',
                permanent: false,
            },
        }
    }
    return {props: {session}}
}

export default Index

Upvotes: 1

Views: 306

Answers (1)

windowsill
windowsill

Reputation: 3649

It looks like there's only ever one board in the redux. You could instead use a namespace so that you don't have to keep swapping different data in and out of the store.

type StoreSlice = {
  [boardId: string]: Board;
}

Then the "brief flash" that you will see will either be the previous data for the correct board, or nothing if it has not yet been fetched.

Upvotes: 2

Related Questions