Home > Back-end >  Automatically refresh fetched data with useSWR after POST request in Next.js application
Automatically refresh fetched data with useSWR after POST request in Next.js application

Time:01-30

I'll try to be as brief as I can since my doubt is quite big.

What I'm trying to do: I'm creating a simple app that reads a JSON file stored locally (%APPDATA% folder, but it doesn't matter for now) that contains an array of objects called TCodes:

[
 {
     "code": "TCODE1",
     "description": "TCode 1",
     "keywords": "tcode1"
 }
]

TCodes are then mapped and showed inside a MUI List in a custom component called TCodesList, placed inside the Home component. Above the TCodesList there is a Toolbar custom component that wraps a input field and an IconButton that opens a MUI Dialog that lets the user create a new TCode and store it in the JSON file.

Reading and writing operations are done with the fs module, I will omit how it is done since it is not relevant. It's enough to know that I do a fetch request to the /api/tcodes endpoint that do different things based on the request verb (GET or POST).

Problem: When I create a new TCode, it is written inside the JSON file, but the TCodesList does not get updated.

Here is the index.js file where the TCodes are read from the file through the useSWR hook:

//imports ...

function useTCodes() {
  const fetcher = (...args) => fetch(...args).then((res) => res.json());

  const { data, error, isLoading } = useSWR('/api/tcodes', fetcher);

  return {
    data: data,
    isLoading: isLoading,
    isError: error
  }
}

export default function Home() {

  const { data } = useTCodes();

  return (
    <div>
      <Toolbar data={data} />  
      <div className={styles.tcodes_container}>
        <TCodesList data={data}/>
      </div>
    </div>
  );
};

The TCodesList component just maps the data passed as a prop and renders each ListItem:

export default function TCodesList({ data }) {
    const filtered = data?.filter((tCode) => {
        return tCode.code.toLowerCase().includes(''.toLowerCase());
    });

    return (
        <List>
            {
                filtered && filtered.map((tCode, index) => (
                    <div key={index}>
                        {index != 0 && <Divider />}
                        <ListItem>
                            <div>
                                <ListItemText className={styles.tcodes_list_item_code}>{tCode.code}</ListItemText>
                                <ListItemText className={styles.tcodes_list_item_description}>{tCode.description}</ListItemText>
                            </div>
                        </ListItem>
                    </div>
                ))
            }
        </List>
    );
}

Here is the AddDialog component where I send the POST request to write the new TCode in the JSON file:

//imports ...
export default function AddDialog({ open, setOpen }) {
    const [tCode, setTCode] = useState('');
    const [description, setDescription] = useState('');
    const [keywords, setKeywords] = useState('');

    const initState = () => {
        setTCode('');
        setDescription('');
        setKeywords('');
    }

    const closeDialog = () => {
        setOpen(false);
    }

    const addTCode = () => {
        fetch('http://localhost:3000/api/tcodes', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                code: tCode,
                description: description,
                keywords: keywords
            })
        });
        initState();
        closeDialog();
    }

    const handleTCodeChange = (event) => {
        setTCode(event.target.value);
    }

    const handleDescriptionChange = (event) => {
        setDescription(event.target.value);
    }

    const handleKeywordsChange = (event) => {
        setKeywords(event.target.value);
    }

    return (
        <Dialog open={open}
            onClose={closeDialog}>
            <DialogTitle>Add new TCode</DialogTitle>
            <DialogContent>
                <TextField value={tCode}
                    onChange={handleTCodeChange}
                    autoFocus
                    fullWidth
                    margin="dense"
                    label="TCode"
                    type="text"
                    variant='outlined' />
                <TextField value={description}
                    onChange={handleDescriptionChange}
                    fullWidth
                    margin='dense'
                    label="Description"
                    type="text"
                    variant='outlined' />
                <TextField value={keywords}
                    onChange={handleKeywordsChange}
                    fullWidth
                    margin='dense'
                    label="Keywords"
                    type="text"
                    variant='outlined' />
            </DialogContent>
            <DialogActions>
                <Button onClick={closeDialog}>Cancel</Button>
                <Button onClick={addTCode}>Add</Button>
            </DialogActions>
        </Dialog>
    );
}

The addTCode function is where the file is updated. In this way I would have expected the SWR package to revalidate the data and update it inside the TCodesList component.

P.S. I'm passing data as props throughout the app. I explored different solutions to better manage the state providing inside the app and to manage the state updating (context, prop drilling, context dispatchers) but it seemed all a bit of an overkill so I'm sticking with SWR right now since it has a few benefits.

I'm probably missing something obvious since I'm quite new to this. Does anyone have any idea on how to accomplish what explained above?

Thanks in advance for the help.

CodePudding user response:

You need to ask SWR to mutate the data after you make your POST request. You can do that by passing mutate returned by useSWR to TCodesList:

//imports ...

function useTCodes() {
  const fetcher = (...args) => fetch(...args).then((res) => res.json());

  const { data, error, isLoading, mutate } = useSWR("/api/tcodes", fetcher);

  return {
    data: data,
    isLoading: isLoading,
    isError: error,
    mutate: mutate,
  };
}

export default function Home() {
  const { data, mutate } = useTCodes();

  return (
    <div>
      <Toolbar data={data} />
      <div className={styles.tcodes_container}>
        <TCodesList data={data} mutate={mutate} />
      </div>
    </div>
  );
}

Then pass this same mutate to AddDialog along with data, to use it inside addTCode, like so:

const addTCode = async () => {
  const newItem = {
    code: tCode,
    description: description,
    keywords: keywords,
  };
  await fetch("http://localhost:3000/api/tcodes", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(newItem),
  });
  // update the local data immediately and revalidate (refetch)
  // NOTE: key is not required when using useSWR's mutate as it's pre-bound
  mutate([ ...data, newItem ]);
  initState();
  closeDialog();
};
  • Related