2

I'm having trouble understanding the correct way of handling data exchange between server-side and client-side components in NextJS 13.

I'm trying to understand this by creating the simplest scenario possible. I have these two functions that simply read and write JSON files on the server:

import fs from 'fs'

export const saveData = async (data) => {
  const jsonData = JSON.stringify(data)
  try {
    await fs.promises.writeFile('data/data.json', jsonData)
  } catch (err) {
    console.log('Error', err)
  }
}

export const getData = async () => {
  try {
    const jsonData = await fs.promises.readFile('data/data.json')
    const data = JSON.parse(jsonData)
    return data

  } catch (err) {
    console.log('Error', err)
    return null
  }
}

A React component that displays the data:

export const SomeComponent = ({ data }) => <p>{data.someKey}</p>

And an interactive React component that gets user input:

'use client'
import { useState } from 'react'

export default function Input({ handleInput }) {
  const [value, setValue] = useState()

  return (
    <div>
      <input type="text" onChange={(e) => setValue(e.target.value)}/>
      <button onClick={() => handleInput(value)}>Save</button>
    </div>
  )
}

If I create a page function to get the data and handle saving new data, I cannot pass the handler to the Input component, because it's a client-side component.

import { SomeComponent } from './components/component'
import { getData, saveData } from './data'
import Input from './components/input'

export default async function Home() {
  const data = await getData()

  const handleInput = (data) => {
    saveData(data)
  }

  return (
    <div>
      <SomeComponent data={data} />
      {/* Error: */}
      <Input handleInput={handleInput}/>
    </div>
  )
}

How, then, is passing data between the client and the server supposed to be handled in this example?

Nick Parsons
  • 45,728
  • 6
  • 46
  • 64
Cirrocumulus
  • 520
  • 3
  • 15

1 Answers1

2

You basically have two options when it comes to this problem:

  1. Option 1: An API that can be called from the client component which updates the file and optimistically updating the local state. Probably the more common approach to this kind of requirement.

  2. Option 2: A server action that which passed to the client component in combination with the new useOptimistic hook. Keep in mind that server actions are still in alpha and not recommended to use in production yet.

I strongly recommend you check the official docs about the limitations and specification of server actions. Here is a simple example what the second option may look like if implemented:

Your server component
const onSave = async (update: any) => {
  "use server";
  const { default: fs } = await import("fs"); 
  await fs.writeJSON("./myfile.json", update);
}

async function ServerComponent(): Promise<JSX.Element> {
  const data = await getMyData();
  return <ClientComponent data={data} onSave={onSave} />;
}

The use server directive is used to define the function as a server action. Keep in mind that server actions do not respect sessions by default and the same function is ran for each request.

Your client component
function ClientComponent(props: Props): JSX.Element {
  const { data: initialData, onSave } = props;
  
  const [data, setData] = useState(initialData);

  const onUpdate = useCallback(async (update: any) => {
    setData(prev => ({ ...prev, ...update }));
    await onSave(update);
  }, [onSave]);

  return // ...
}

You will have to activate server actions inside your next config file due to their current alpha state, otherwise this example will not work.

Fabio Nettis
  • 1,150
  • 6
  • 18
  • Thanks. So if I understand you correctly, even if I'm not using any external backend and just want to read and write files based on user input, I should implement a barebones custom API just for this purpose? – Cirrocumulus Jun 21 '23 at 11:34
  • That is exactly what I mean, yes. You can call the API handler logic directly from the server component by defining your logic separately and importing it into your server component and API. Making it reusable. – Fabio Nettis Jun 21 '23 at 11:38
  • OK, thanks again. – Cirrocumulus Jun 21 '23 at 16:03