19/ Updating the userdata in Strapi and NextAuth
Tags: nextauth, strapi, nextjs
Published: 2024-04-01 16:36:38

We already build a form and a custom `Strapi` endpoint to update the user. We also established that we need to call this endpoint in a server action. But, till now, we've been using `useFormState` to handle server actions. We can't do so here.

The code for this chapter is available on [github (branch changeusername)](https://github.com/peterlidee/NNAS/tree/changeusername).

## Another complication

Why not? Because on success, when our server action returns a success, we will have to handle some stuff client-side. This has to do with updating our `NextAuth` token, we will see this in a bit. So, client side, we have to listen to the actionResponse. On success we do one thing, on error another. But where do we listen for this?

I will illustrate this with a simple example. Suppose we have a form, `useFormState` and a server action and we want to log the return value of the server action in our browser console but only on success.

```jsx
const [state, formAction] = useFormState(serverAction, initialState)
// form
<form action={formAction}></form>
```

How would you do this? How do you get the successful state into the browser console? You may be tempted to listen for the state inside the function component:

```jsx
if (!state.error && state.message === 'Success') {
  console.log(state);
}
```

But this will also log when there is another piece of state that updates or when f.e. the parent rerenders. So, it's not good. The solution is quite simple:

> Server Actions are not limited to `<form>` and can be invoked from event handlers, useEffect, third-party libraries, and other form elements like `<button>`. ([Next docs](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#behavior))

This means that we can just do this:

```jsx
function handleSubmit(){
  // do some stuff
  const res = await serverAction()
}
<form submit={handleSubmit}></form>
```

or this using action:

```jsx
<form action={handleSubmit}></form>
```

The difference here would lie in the fact that when using action, you won't have to save form state in a `useState` hook.

To conclude this section: we will use a server action but we can't use `useFormState`. We will call our server action from inside a normal handleSubmit function. This will allow us to listen for the response and do something client-side, based upon the response. Downside is that it will require us to handle state and loading manually.

## editUsernameAction

Let's write our server action first. Remember, as we won't call it using `useFormState` nor directly using the form action attribute, it won't automatically receive any arguments like formData or prevState. We will pass it one argument: username (the new username from our form) For the rest it's what were used to: calling `Strapi` and handling error and success.

```jsx
// frontend/src/components/auth/account/editUsernameAction.ts

'use server';

import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/authOptions';
import { StrapiErrorT } from '@/types/strapi/StrapiError';

type ActionErrorT = {
  error: true;
  message: string;
};
type ActionSuccessT = {
  error: false;
  message: 'Success';
  data: {
    username: string;
  };
};
export type EditUsernameActionT = ActionErrorT | ActionSuccessT;

export default async function editUsernameAction(
  username: string
): Promise<EditUsernameActionT> {
  const session = await getServerSession(authOptions);
  try {
    const strapiResponse = await fetch(
      process.env.STRAPI_BACKEND_URL + '/api/user/me',
      {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${session?.strapiToken}`,
        },
        body: JSON.stringify({
          username,
        }),
        cache: 'no-cache',
      }
    );

    // handle strapi error
    if (!strapiResponse.ok) {
      const response: ActionErrorT = {
        error: true,
        message: '',
      };
      // check if response in json-able
      const contentType = strapiResponse.headers.get('content-type');
      if (contentType === 'application/json; charset=utf-8') {
        const data: StrapiErrorT = await strapiResponse.json();
        response.message = data.error.message;
      } else {
        response.message = strapiResponse.statusText;
      }
      return response;
    }

    // handle strapi success
    const data = await strapiResponse.json();
    return {
      error: false,
      message: 'Success',
      data: {
        username: data.username as string,
      },
    };
  } catch (error: any) {
    return {
      error: true,
      message: 'message' in error ? error.message : error.statusText,
    };
  }
}
```

So, this action returns an error or a success object. Note that the success object has a data attribute with the username on it. Also note that we take the username from the strapiResponse, not from our function argument. This means that we uphold the single state principle. Our data comes from our database, not from an input form. This shouldn't be a problem here but it's something you should keep in mind.

One tiny detail missing in our server action but let's first rig up our from component with the action.

## handleSubmit

In this component, I will use the onsubmit form attribute. This is a bit more complex but I want to show how it's done. In the next chapter we will have a similar flow where we call handleSubmit with the action attribute.

Since we use onSubmit, we have to manually put some things in state: newUsername, message, error and loading. We also attach our input to the newUsername state, making it a controlled input.

Next, our handleSubmit function:

```jsx
async function handleSubmit(e: FormEvent) {
  e.preventDefault();

  setLoading(true);

  // validate newUsername
  if (newUsername === '' || newUsername.length < 4) {
    setError('Username is too short.');
    setLoading(false);
    return;
  }

  // call server action
  const actionResponse: EditUsernameActionT = await editUsernameAction(
    newUsername
  );

  // handle error
  if (actionResponse.error) {
    setError(actionRes.message);
    setMessage(actionRes.message);
    setLoading(false);
    return;
  }

  // handle success
  if (!actionResponse.error && actionResponse.message === 'Success') {
    // inform user of success
    setError(null);
    setMessage('Updated username.');
    setLoading(false);

    // TODO update session and token?
  }
}
```

This should all be clear. We set loading. Do some basic validation of the form input. Then we call the server action. We await it because it is an async function and we pass it the newUsername from state. We check our actionResponse and set state accordingly.

## Update form

We update our form component to show the message, error and loading states:

```jsx
<form onSubmit={handleSubmit}>
  <label htmlFor='username' className='block italic'>
    Username:
  </label>
  <div className='flex gap-1'>
    {!edit && <div>{username}</div>}
    {edit && (
      <>
        <input
          {/*...*/}
          value={newUsername}
          onChange={(e) => setNewUsername(e.target.value)}
        />
        <button
          type='submit'
          {/*...*/}
          disabled={loading}
          aria-disabled={loading}
        >
          {loading ? 'saving' : 'save'}
        </button>
      </>
    )}
    <button
      type='button'
      onClick={() => {
        setEdit((prev) => !prev);
        setError(null);
        setMessage(null);
        setNewUsername(username);
      }}
      className='underline text-sky-700 ml-1'
    >
      {edit ? 'close' : 'edit'}
    </button>
  </div>
  {edit && error && (
    <div className='text-red-700' aria-live='polite'>
      Something went wrong: {error}
    </div>
  )}
  {edit && !error && message ? (
    <div className='text-green-700' aria-live='polite'>
      {message}
    </div>
  ) : null}
</form>
```

Note that we also updated the toggle button. On toggle close, we reset error and message and reset newUsername to the username prop (the original).

Everything that is in here now works. If we run our app, sign in a go to the `/account` page, we can toggle open the edit username component and change the name. It will display a success or error message the button has a loading state.

![edit username success](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/02i51h399z1osx141mon.png)


![edit username error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/umck2trryuhda5zas9zg.png)

However, as it stands now, there is a problem. When we toggle our edit username component closed, the old username is still displayed. We talked about this problem before but haven't implemented the solution yet.

We display props.username. This comes from or `<Account />` component that gets it's data from our api fetch. When we updated our username we should also revalidate said fetch. This is easy. We go back to our editUsernameAction and add this one line before we return our success object:

```jsx
  // handle strapi success
  revalidateTag('strapi-users-me');
  const data = await strapiResponse.json();
  return {
    error: false,
    message: 'Success',
    data: {
      username: data.username as string,
    },
  };
```

We test it out again and it works like a charm. When updating the username and toggling the component closed, it now shows the updated username because `Next` revalidated our query.

Small issue here. There is a UI flicker. But, don't worry about this, it is a dev mode issue. It goes away in production mode.

## New problems

Are we done now? No! We are yet to update our `NextAuth` session and token.

![NextAuth session not updated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yow4ccg6jhgbycgucaih.png)

Notice our updated username _John_, while our navbar user and `<LoggedInClient />` and `<LoggedInServer />` components (the green ones) still show the old username _Frank_. That's what we need to solve now.

## NextAuth update function

Remember the `NextAuth` `useSession` hook? We haven't actually used it, only in `<LoggedInClient />`. But we do need it here. Besides data, `useSession` returns 2 more properties: status and a function called `update`.

```jsx
'use client';
const { data: session, status, update } = useSession();
```

The `update` function will be our hero for now. It allows you to hook into the `NextAuth` callback flow that we discussed in detail in earlier chapters. The `jwt callback` serves to put items onto the `NextAuth` token. Until now, we've used `jwt` to take items from the GoogleProvider or CredentialsProvider and put them on the token: strapiToken, strapiUserId, provider and blocked. We only did this on _sign in_, when the `jwt callback` account argument is populated.

Another argument of the `jwt callback` is called trigger. It has 3 potential values: `signUp`, `signIn` and `update`. When calling the update function, trigger will equal 'update'. You should be able to figure out the flow now. We need to update the token (and the session). To update the token, you use the `jwt callback`. Calling `update` lets you hook into the `jwt callback`.

So, we will call the `update` function in our `<EditUsername />` component and pass it the new username that the server action `updateUsernameAction` returned: `update(actionResponse.data.username)`. We then listen for the trigger argument in our `jwt` and when it equals 'update' we update the token.

```js
// inside jwt callback
if (trigger === 'update') {
  // update token
}
```

We passed the new username into our `update` function but where do we retrieve it in our `jwt callback`?

> When using SessionOptions.strategy "jwt", this is the data sent from the client via the useSession().update method. (source: TypeScript tooltip when hovering the session argument)

So, it's on the session argument of the `jwt callback`. We can then update our code like this:

```jsx
// inside jwt callback
if (trigger === 'update' && session.username) {
  // update token
  token.name = session.username;
}
```

And that is it. We don't need to do anything else. The `jwt callback` has now updated our token. We don't need to alter the `session callback`.

Note 1: notice how we added a conditional: `&& session.username`. This allows us to have multiple uses for `update`. F.e. next time we call it with `update({foo: 'bar'})` and then listen for `&& session.foo`.

Note 2: naming this `jwt callback` argument 'session' seems confusing to me. Don't we already have a session? (returned from `useSession` or `getServerSession`). But, that is what `NextAuth` called it and there is nothing I can do about that.

Note 3: in case you want to do something to the token on _sign in_, listen for the trigger to equal `signIn` inside the `jwt`.

## Quick recap

1. We fetch the current user data in our `<Account />` component using the `Strapi` endpoint `users/me`.
2. A form allows the user to enter a new username.
3. We had to write a custom `Strapi` api endpoint `/user/me` to be able to update the current user.
4. The endpoint is called inside a server action editUsernameAction.
5. We have to use a server action because on successfully updating the username, we also need to update our fetch (step 1). We use `revalidateTag` inside the server action.
6. The `update` function lets us update our `NextAuth` token/session. This is a client-side function.
7. To switch from the server action (server side) to the update function (client side) we call the server action as a function inside a submit handler. We cannot use `useFormState`.
8. Inside the submit handler, our server action will return us the updated username from the database.
9. We then call update and pass it this username.
10. To catch this update call, we listen for `trigger="update"` and `session.username` inside the `jwt callback`. We can then update our token with the new username.

## How to update getServerSession

Let's test this out to see if everything works. We have a user Frank and we update the name to Franky:

![NextAuth getServerSession not updated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awz3phxrxq6w8bvvsute.png)

No success. The navbar username is still Frank as is the `<LoggedInServer />` component ("Server: logged in as Frank."). The `<LoggedInClient />` did update: "Client: logged in as Franky." So, `useSession` updated but `getServerSession` did not. Note: when we do a page refresh, do username does show 'Franky' in all components!

Good news, we encountered this problem before in the `<SignInForm />` component. We solved it by calling `router.refresh()` which refreshes all data request and rerenders all server components. Great, we add it in our submit handler after we call update:

```jsx
// update NextAuth token
await update({ username: actionResponse.data.username });
// refresh server components
router.refresh();
```

And everything works! Note that we have to await update. Else, `router.refresh` would be called before update has finished doing its work. As a reminder, calling `router.refresh` causes a UI flicker but only in dev mode.

You can see the final version of the [editUsernameAction](https://github.com/peterlidee/NNAS/blob/changeusername/frontend/src/components/auth/account/editUsernameAction.ts), [`<EditUsername />` component](https://github.com/peterlidee/NNAS/blob/changeusername/frontend/src/components/auth/account/EditUsername.tsx) and the [`jwt callback`](https://github.com/peterlidee/NNAS/blob/changeusername/frontend/src/app/api/auth/%5B...nextauth%5D/authOptions.ts#L84) on github.

## Conclusion

This was a massive amount of code for such a small component. The main culprit is the switching we have to do between client-side and server-side code. `NextAuth v4` does not work well with server components. I hope `NextAuth v5` which is still in beta, will solve these problems.

One last thing is left in our auth flow: letting a signed in user change his or hers password. That is what we will do in the next chapter.

---

If you want to support my writing, you can [donate with paypal](https://www.paypal.com/donate/?hosted_button_id=4D78YQU4V5NEJ).