Next.js Discord

Discord Forum

Best Way to Structure Client and Server Components for Server Actions

Unanswered
Shih Tzu posted this in #help-forum
Open in Discord
Shih TzuOP
Initially, I had an issue with the useState hook not updating the state with the new value passed through props. I solved this issue by using useEffect to update the state based on the props - is this the correct way to achieve this? also, would you recommend that i have a server component like this
<form action={serverAction}>
    <ClientComponent />
</form>
and the client components have the inputs but the action is submitted from the server component

or

<div>
    <ClientComponent />
</div>
and the client component has everything to do with the form and submits the action from there

do you think it makes a big difference? which one, if any of them, has an advantage?
@English Angora

128 Replies

Shih TzuOP
@/quiz/[id]/page.jsx
export default async function editQuiz({ params }){
    // query database for current data
    return (
        <div className="px-20 py-10 mx-auto max-w-screen-lg">
            <h2 className="text-2xl font-semibold mb-4 ml-4">Quiz Editor</h2>
            <form action={updateQuiz}>
                <QuizForm id={params.id} name={quiz.name} category={quiz.category} description={quiz.description} questionsList={questionsList}/>
            </form>
        </div>
    )
};


@/components/QuizForm.jsx
export default function QuizForm({ id, name, category, description, questionsList }) {
    console.log(questionsList);
    const [questions, setQuestions] = useState(questionsList);
    console.log(questions);
    const [quizInfo, setQuizInfo] = useState({
        name: name,
        category: category,
        description: description,
    });

    useEffect(() => {
        setQuestions(questionsList);
        setQuizInfo({
            name: name,
            category: category,
            description: description,
        });
    }, [questionsList, name, category, description])
  return (
  )


It didn't work before adding the useEffect
@English Angora why are you setting state twice?
Shih TzuOP
How it works is I have a server action that is triggered by the click of a button. This sends lots of input data to the "backend", where I perform some db operation. At the end I revalidate this path. Then the data querying re-runs as expected. Until now, everything is how it should be. But then, with the new queried data that is passed from editQuiz to the QuizForm, the QuizForm's state is not updating. Initially, I thought it was some client-side cache. But then, I searched stackoverflow and it said that I need to useEffect to update the state.
@English Angora and how is QuizForm not working related to editQuiz
Shih TzuOP
it is working now, with this useEffect solution
is this the "correct" solution?
@English Angora and how are you querying database in editQuiz??
Shih TzuOP
export default async function editQuiz({ params }){

    // query database for current quiz data

    const cookieStore = cookies();
    const supabase = createServerComponentClient({cookies: () => cookieStore});
    const {data: {session}} = await supabase.auth.getSession();
    const user = session?.user;
    const {data: quizzes, error: quizError} = await supabase
        .from('quiz')
        .select('*')
        .eq('user_id', user.id)
        .eq('id', params.id)
    if (quizError) {
        console.error("error fetching quiz data");
    } 
English Angora
I still can't see where is the quiz variable you are sending to QuizForm
@Shih Tzu is this what you're looking for or is it something else which i haven't sent?
English Angora
you haven't sent the quiz variable:
                <QuizForm id={params.id} name={quiz.name} category={quiz.category} description={quiz.description} questionsList={questionsList}/>
nor questionsList
Shih TzuOP
is there a way for me to send longer messages than what discord allows?
export default async function editQuiz({ params }){
    // query database for current quiz data (need to debug)
    const cookieStore = cookies();
    const supabase = createServerComponentClient({cookies: () => cookieStore});
    const {data: {session}} = await supabase.auth.getSession();
    const user = session?.user;
    const {data: quizzes, error: quizError} = await supabase
        .from('quiz')
        .select('*')
        .eq('user_id', user.id)
        .eq('id', params.id)
    if (quizError) {
        console.error("error fetching quiz data");
    } 
    const quiz = quizzes[0];

    const {data: questionsData, error: questionError} = await supabase
        .from('question')
        .select('*')
        .eq('quiz_id', quiz.id)
        .order('question_number', { ascending: true })
    if (questionError) {
        console.error("error fetching questions")
    }
    const questionsList = [];
    for (let index = 0; index < questionsData.length; index++) {
        const question = questionsData[index];
        // For each question, iterate through the choices
        let questionObject = {};
        const {data: choicesData, error: choicesError} = await supabase
            .from('choice')
            .select('*')
            .eq('question_id', question.id)
            .order('id', { ascending: true })
        if (choicesError) {
            console.error("error fetching choices");
        }
        console.log(choicesData);
        let questionChoices = [];
        for (let choiceIndex = 0; choiceIndex < choicesData.length; choiceIndex++){
            const choice = choicesData[choiceIndex];
            if (choice.is_correct){questionChoices.unshift(choice.choice_text);
            } else {questionChoices.push(choice.choice_text);
            }
        }
        questionObject.questionText = question.question_text;
        questionObject.choices = questionChoices;
        questionsList.push(questionObject);
    }
    console.log(questionsList);
@Shih Tzu is there a way for me to send longer messages than what discord allows?
English Angora
yeah just send it as a file
Shih TzuOP
The quiz variable and questionsList are being updated re-fetched on the server after the server action completes, since the path is revalidated. After this, they are correctly passed as props to the EditQuiz component. However, after that, the state of the EditQuiz component doesn't automatically change based on the new props passed in. Instead, I have to use useEffect. Is this normal?
Original message was deleted
English Angora
no it should be the same
sec
function Component({props}) {
  const [something,setSomething] = useState(props.something);
  return (
    <>
      {something}
    </>
  )
}

I am 100% sure this component will be rerenderd on props change
Original message was deleted
Shih TzuOP
I would like it to reset, meaning that the current value is deleted and it sets it to the new value which is the prop that is passed in. How, if I can, can I achieve this without useEffect? Is there a standard way to do this? Is this more of a React question than a Next.js-specific question?
that's the reason component is re rendered
export default function QuizForm({ id, name, category, description, questionsList }) {
    const [questions, setQuestions] = useState(questionsList);
    const [quizInfo, setQuizInfo] = useState({
        name: name,
        category: category,
        description: description,
    });
  return (
  )
try this
make sure all the props you are sending in QuizForm are awaited
@Shih Tzu i cannot immediately see the change you made (if you made one). could you point it out to me?
English Angora
i removed the useEffect completely and console logs
Shih TzuOP
this is how it was originally, but what happened here is that it didn't update the display
therefore, i added in console logs and saw that the prop was changing but the state wasn't
English Angora
are you sure?
Shih TzuOP
yes
the way i found the bug is by updating the database with static text, as opposed to the input
i noticed that the text still remained what the input was and didn't change to the static text that was hardcoded in the server action
English Angora
can you make editQuiz be a client component function
and fetching data in a seperate component
Sounds very weird that the client component doesn't rerender when their props change if its in server component but might be a case
I will try to do it myself
gimme a second
in you're case you;ve said that doesn't work for you tho?
@English Angora yeah that's what I;ve said
Shih TzuOP
can you point out exactly which point on this page you mentioned
i probably didn't understand you if you did mention it
@Shih Tzu can you point out exactly which point on this page you mentioned i probably didn't understand you if you did mention it
English Angora
export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

this
This is inefficient because ProfilePage and its children will first render with the stale value, and then render again. It is also complicated because you’d need to do this in every component that has some state inside ProfilePage. For example, if the comment UI is nested, you’d want to clear out nested comment state too.
yeah sorry dude no idea
never had an issue like that
everything works for me
Shih TzuOP
Since this might be more of a React-specific issue, I asked in the React server, and someone said that the problem I had is normal and that's how React works.
100%
this has to do with next.js
Shih TzuOP
yes but on each rerender, is the state set to the prop? or does the state stay as it was previously
@Shih Tzu yes but on each rerender, is the state set to the prop? or does the state stay as it was previously
English Angora
on each prop change the component is rerendered ofc
look
Shih TzuOP
ok, so useState only sets the initial state when the component first renders but not on subsequent renders

is the above statement not true?

i said the above in the React server and the helper replied yes
@Shih Tzu https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes
Shih TzuOP
it says: "Normally, React preserves the state when the same component is rendered in the same spot."
English Angora
here you gop
page.tsx
"use client";

import { ChangeEvent, useState } from "react";
import Client from "./components/Client";

export default function Home() {
  const [name, setName] = useState("john");

  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    setName(event.target.value);
  }
  return (
    <main className="flex min-h-screen flex-col items-center p-24">
      <input
        type="text"
        placeholder="name"
        onChange={(e) => {
          handleChange(e);
        }}
      />
      <Client name={name} />
    </main>
  );
}
Client.tsx
"use client";
interface Props {
  name: string;
}
export default function Client({ name }: Props) {
  return (
    <div>
      <h1>Hi I am a simple component {name}</h1>
    </div>
  );
}
Shih TzuOP
my state is in the child component
English Angora
read what i've sent and check the video
Shih TzuOP
in this case, you are changing the state by typing and the state is in the parent component
that changed state is now being passed as props to the child component which is displaying the change because the state isn't in the child component that is being re-rendered

i'm a beginner in react/next.js so please correct if i am wrong, but i think this is a distinction to make
English Angora
you always lift state up
Shih TzuOP
i can't lift state up to the server component!
English Angora
if you are a begineer you shouldnt touch server compnents
I don't even know what do you want to do, its been an hour good luck
Shih TzuOP
Could someone please take over with the help and explain to me if there is anything I should change? I have posted any relevant code above in this help forum. Ask if you need more context to help me effectively.
Shih TzuOP
Hello
This is the current state of question:

I have a server component that is fetching data.
This server component has a client component within it.
Therefore, the server component passed the data fetched to the client component via props.
The client component is supposed to use these props to set its state.
The purpose of setting state is for an interactive form with buttons.
Then the client component will submit a server action.
The server action does some data processing (the same one that took 10 seconds) and then revalidates the path successfully.
Therefore, the data fetching on the server component happens again and this is passed as props to the client component as expected.
Now, here's the problem: The state of the client component is not updating according to the new data being passed in as props.
How can I fix this? Is this correct behaviour of react?
Toyger
can you give me screenshot of form
Shih TzuOP
Shih TzuOP
Here's a general question:
Any time I want to fetch data on the server, then use this data for a client component using useState, and then a server action is made causing the data to change, and then refetched on server, how to make client component also update these changes accordingly on the UI?
Toyger
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#server-side-validation-and-error-handling
as you can see nextjs have example here, you can get result of your server action into state variable
const [state, formAction] = useFormState(createUser, initialState)

in your server action you need to return some result of your function
return {
    message: 'Please enter a valid email',
  }


with that you can check if you get some result from your function and if yes then update client accordingly
Shih TzuOP
Does useState only set the set to the initial value when the component is rendered for the first time and not on subsequent re-renders?
Toyger
it's not about useState at all, useState is client side only thing
server action is basically backend code, so only way you can communicate is with this useFormState nextjs function that will get result from backend function.
@Shih Tzu If I implement this, will I now not need to revalidate the path?
Toyger
not sure about revalidate, it still can preservecaches and brake some logic, so you'll need to test it without revalidate later when it will work at least somehow
Toyger
ok
Shih TzuOP
import QuizForm from '../../components/QuizForm';
import { cookies } from 'next/headers';
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';


export default async function editQuiz({ params }){

    // query database for current quiz data (need to debug)

    return (
        <div className="px-20 py-10 mx-auto max-w-screen-lg">
            <h2 className="text-2xl font-semibold mb-4 ml-4">Quiz Editor</h2>
            <QuizForm id={params.id} name={quiz.name} category={quiz.category} description={quiz.description} questionsList={questionsList}/>
        </div>
    )
};

This is the server component.
"use client";

import { updateQuiz } from "../actions/updateQuiz";
import { deleteQuiz } from "../actions/deleteQuiz";
import { useState, useEffect } from "react";
import Question from "./Question";
import Link from 'next/link';

export default function QuizForm({ id, name, category, description, questionsList }) {
    const [questions, setQuestions] = useState(questionsList);
    const [quizInfo, setQuizInfo] = useState({
        name: name,
        category: category,
        description: description,
    });
    <form action={serverAction}>
    </form>

This is the client component with the form. The form input fields change based on the state stored managed by the useState hook.
"use server"

import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"
import { revalidatePath } from "next/cache"
import { cookies } from "next/headers"
import { redirect } from "next/navigation"

export async function updateQuiz(formData) {
    const cookieStore = cookies();
    const supabase = createServerComponentClient({cookies : () => cookieStore});
    const {data: {session}} = await supabase.auth.getSession();
    const user = session?.user;

    if (!user) {
        console.error("User is not authenticated - updateQuiz server action");
        return;
    }

    const id = formData.get('id');
    const name = formData.get('name');
    const category = formData.get('category');
    const description = formData.get('description');

    // update quiz info
    const { data: quizData, error: quizError } = await supabase
        .from('quiz')
        .update({name, category, description})
        .match({id, user_id: user.id})
        .select();

    // add question and choices to db

    // revalidatePath
}

This is the server action.
Let's start from the server action. The server action adds some records to the database. Since I call revalidatePath, it then re-renders the server component and causes the data to be re-fetched on the server inside the server component. This new data is being passed as props to the client component which is also re-rendered. The new props that the client component has are not reflected in the state however, and the state still stays as it was before the client component was re-rendered. Is this supposed to happen? My question is specifically about how useState works in response to the component re-rendering and getting different props as data input.
My current workaround is using a useEffect to run when the component is re-rendered with the new props, which then sets the state accordingly. This works, but is this the recommended way? I'd assume not, since React does not encourage the use of useEffect, as shown here: https://react.dev/learn/you-might-not-need-an-effect.
Toyger
it's decent, but imo it's just a bit wrong.
if it works for your use case then probably better to leave it as is.
because to change it there can be some significant changes, like update only specific fields, adding this useFormState, to getting result from server action, probably adding some server props, etc...
Shih TzuOP
Now I think I understand what useFormState does.
The only problem is that the 'Add Question' button (and others) are used to modify the current state without any involvement of the server. How can I manage to do this with useFormState?
@Shih Tzu The only problem is that the 'Add Question' button (and others) are used to modify the current state without any involvement of the server. How can I manage to do this with useFormState?
Toyger
why do you want to involve server in that?
it's basically client part, you need server only when you want to save changes, not when you in process of doing changes.
@Toyger why do you want to involve server in that? it's basically client part, you need server only when you want to save changes, not when you in process of doing changes.
Shih TzuOP
I'm saying that I don't involve the server.
Just realised: useFormState can also be used just on client side?
@Shih Tzu I'm saying that I don't involve the server. Just realised: useFormState can also be used just on client side?
Toyger
useFormState can also be used just on client side?
the only reason to use it to get data from server
Shih TzuOP
then how do I add a question with the add question button?
currently how it works is it updates the state with another question
then there's a map which iterates through all the questions in the current state and renders a component
https://stackoverflow.com/questions/68730502/react-usestate-with-state-that-updates-based-on-props
this is the stackoverflow post that made me go with the useEffect solution
it also says: "there's not generally a great reason to initialize state with a prop"
@Shih Tzu then how do I add a question with the add question button? currently how it works is it updates the state with another question then there's a map which iterates through all the questions in the current state and renders a component
Toyger
you can have additional client state like
const [addformvisible,setAddformvisible] = useState(false);

and then in jsx
{addformvisible && <>form with fields to add new question</>}


so all changes happen on client.
then when user add new question and click "Save" then it sends data from form to update it on server.
"there's not generally a great reason to initialize state with a prop"
it have it pros and cons, so for your case it's now not a significant problem
@Shih Tzu Why do I need to change the visibility of the form?
Toyger
if I understood your logic correctly for "add question" it should just provide fields for another question to fill in
Shih TzuOP
yes, and this should clicking the add question button twice should have two more question inputs within the same overall form for the overall quiz within which there are new questions being added
@Shih Tzu yes, and this should clicking the add question button twice should have two more question inputs within the same overall form for the overall quiz within which there are new questions being added
Toyger
ok than it not just visibility, it should be some state with amount of additional components like <AddQuestionComponent/> displayed or something like that
Shih TzuOP
I think I'll just keep it as it is to avoid increasing any complexity.
In general though, if I watch to fetch data on the server and then show that data within a client component, do I pass it through props?
Shih TzuOP
In my case, I probably don't even need the useEffect. This is because the data returned by the server action is going to be the same as the data that was inputted. A scenario where I would need it is where the client-side input is deliberately different to the server side processing.
Shih TzuOP
This is how I've used useEffect.
const [questions, setQuestions] = useState(questionsList);
const [quizInfo, setQuizInfo] = useState({
    name: name,
    category: category,
    description: description,
});

useEffect(() => {
    setQuestions(questionsList);
    setQuizInfo({
        name: name,
        category: category,
        description: description,
    });
}, [questionsList, name, category, description])
Toyger
you probably then don't even need this
const [questions, setQuestions] ...
const [quizInfo, setQuizInfo] ...

you can get all your data directly from props then
Shih TzuOP
export default function QuizForm({ id, name, category, description, questionsList }) {
    const [questions, setQuestions] = useState(questionsList);
    const [quizInfo, setQuizInfo] = useState({
        name: name,
        category: category,
        description: description,
    });

like this?
@Shih Tzu jsx export default function QuizForm({ id, name, category, description, questionsList }) { const [questions, setQuestions] = useState(questionsList); const [quizInfo, setQuizInfo] = useState({ name: name, category: category, description: description, }); like this?
Toyger
you need to get rid of this too
where you used quizInfo.category you pass now just category because you destructured it there already { id, name, category, description, questionsList }, where you use questions pass questionsList and others change to non state but props variables
Shih TzuOP
But this was exactly my issue. Getting the data from props and setting it in the state with useState did not work. This is because useState only sets the initial state when the component is first rendered, not on subsequent re-renders so it does not reflect the changes with the new props that are passed in when the props change and the component re-renders.
Shih TzuOP
I do, because there's the form which I need to update the input fields for by typing which updates the state.
Toyger
ok, then probably better leave it as is, I am not sure that I understand it correctly right now.
Shih TzuOP
Also, would you recommend that i have a server component like this
<form action={serverAction}>
    <ClientComponent />
</form>
and the client components have the inputs but the action is submitted from the server component

or

<div>
    <ClientComponent />
</div>
and the client component has everything to do with the form and submits the action from there

do you think it makes a big difference? which one, if any of them, has an advantage?
@Shih Tzu Also, would you recommend that i have a server component like this jsx <form action={serverAction}> <ClientComponent /> </form> and the client components have the inputs but the action is submitted from the server component or jsx <div> <ClientComponent /> </div> and the client component has everything to do with the form and submits the action from there do you think it makes a big difference? which one, if any of them, has an advantage?
Toyger
ideally it should be decoupled like in nextjs example
'use client'
 
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: null,
}
 
export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button>Sign up</button>
    </form>
  )
}


as you see they have everything as client component, only thing from their example instead formAction you can probably pass createUser directly, so without useFormState you just pass server action
I found a solution in this article.
I can pass in a key to each quizform element. In this way, even if the component is re-rendered in the same position, it will update the state.
Currently, my key is a Math.random(). Is there a better key I can use? Possibly the current datetime?
@Shih Tzu Currently, my key is a Math.random(). Is there a better key I can use? Possibly the current datetime?
Toyger
probably something like uuid give more randomness https://bobbyhadz.com/blog/react-generate-unique-id
but sometimes even math.random is enough.