Retour • 07/10/2024
Comment fonctionnent les server-actions en NextJS ?
Écris par Melvyn Malherbe le 07/10/2024
Les Server Actions
en NextJS sont devenues incontournables mais beaucoup de personnes trouvent qu'elles sont "magiques" alors que non. Derrière les server-actions, il y a du code, de la logique et ensemble nous allons venir démystifier les Server-Action
.
C'est quoi une server-action (en bref) ?
Pour rappel, les server-actions interviennent quand tu utilises "use server"
dans un fichier, par exemple une fonction qui permet de modifier le username d'un utilisateur.
'use server';
export const updateUsernameAction = async (id, username) => {
const user = await prisma.user.update({
where: {
id,
},
data: {
username,
},
});
return user;
};
Cette méthode peut ensuite être appelée dans ton front-end comme n'importe quelle méthode :
'use client';
const App = () => {
return (
<button onClick={() => updateUsernameAction(1, 'Melvynx')}>
Update Username
</button>
);
};
Mais par quelle magie NextJS
arrive-t-il à définir d'où vient cette méthode updateUsernameAction
et surtout quelle méthode appeler ?
Pour en savoir plus, je t'invite à jeter un œil à mon article complet sur le sujet.
Comment NextJS
trouve les server-actions ?
Imaginons une server-action qui s'appellerait getServerActionBackendData
, NextJS va regarder toutes les méthodes que notre page appelle et va les inclure dans le bundle JavaScript.
En regardant le fichier page.js
qu'on reçoit quand on charge la page, on peut voir ceci :
On peut voir que dans le bundle JavaScript, on voit clairement la méthode getServerActionBackendData
qui est ajoutée. Celle-ci est définie comme ça :
var getServerActionBackendData = () => {
private_next_rsc_action_client_wrapper__WEBPACK_IMPORTED_MODULE_1__.createServerReference(
'0008bfae0df0222acb8b0c3443136c4c'
);
};
Cette méthode va être injectée dans notre page pour que quand notre client l'appelle, elle soit clairement définie.
On peut voir qu'elle appelle une méthode, createServerReference
qui est définie dans le fichier [react-server-dom-turbopack-client.browser.development.js
] :
exports.createServerReference = function (id, callServer) {
function action() {
var args = Array.prototype.slice.call(arguments);
return callServer(id, args);
}
registerServerReference(action, { id: id, bound: null });
return action;
};
On peut voir ici que ce que la méthode va faire, c'est prendre un id
puis prendre une méthode callServer
, qu'on va voir ensuite. Ce que fait la méthode action
c'est prendre tous les arguments avec lesquels on appelle cette méthode et venir appeler callServer
avec tous ces arguments et ce fameux id
.
Les méthodes avec la syntaxe function
peuvent accéder à une variable arguments
qui contient tous les arguments passés à la fonction.
Ensuite la méthode callServer
est appelée avec l'id unique de notre action, et nos différents arguments. Et justement... que fait callServer
:
// Le code est rendu plus court pour la lisibilité de l'article
const serverActionDispatcher: ServerActionDispatcher = useCallback(
(actionPayload) => {
startTransition(() => {
dispatch({
...actionPayload,
type: ACTION_SERVER_ACTION,
})
})
},
[dispatch]
)
const globalServerActionDispatcher = serverActionDispatcher;
export async function callServer(actionId: string, actionArgs: any[]) {
const actionDispatcher = globalServerActionDispatcher
if (!actionDispatcher) {
throw new Error('Invariant: missing action dispatcher.')
}
return new Promise((resolve, reject) => {
actionDispatcher({
actionId,
actionArgs,
resolve,
reject,
})
})
}
callServer
vient venir retourner une promise qui appelle un "dispatcher" avec le type ACTION_SERVER_ACTION
et les arguments passés à la fonction.
Ce qui vient appeler un client reducer
:
/**
* Reducer that handles the app-router state updates.
*/
function clientReducer(
state: ReadonlyReducerState,
action: ReducerActions
): ReducerState {
switch (action.type) {
// Autre cas...
case ACTION_SERVER_ACTION: {
return serverActionReducer(state, action)
}
// This case should never be hit as dispatch is strongly typed.
default:
throw new Error('Unknown action')
}
}
Qui vient finalement appeler une méthode serverActionReducer
en passant en premier paramètre le state et en deuxième l'action. Et justement... que fait serverActionReducer
:
export function serverActionReducer(
state: ReadonlyReducerState,
action: ServerActionAction
): ReducerState {
const { resolve, reject } = action
const mutable: ServerActionMutable = {}
const href = state.canonicalUrl
let currentTree = state.tree
const nextUrl =
state.nextUrl && hasInterceptionRouteInCurrentTree(state.tree)
? state.nextUrl
: null
return fetchServerAction(state, nextUrl, action).then(
async ({
actionResult,
actionFlightData: flightData,
redirectLocation,
redirectType,
isPrerender,
revalidatedParts,
}) => {
// Va venir gérer le résultat
// Si la server-action est une redirection = on redirige
// Si la server-action vient revalidate des données = on modifie les données
// Si la server-action retourne des données = on les retourne
},
(e: any) => {
// When the server action is rejected we don't update the state and instead call the reject handler of the promise.
reject(e)
return state
}
)
}
Cette méthode s'occupe uniquement d'appeler une autre méthode fetchServerAction
et de récupérer son résultat. Il faut savoir qu'une action peut faire plusieurs choses :
revalidate
des données avecrevalidatePath
redirect
vers une autre pagereturn
des données
Maintenant, que fait fetchServerAction
?
async function fetchServerAction(
state: ReadonlyReducerState,
nextUrl: ReadonlyReducerState['nextUrl'],
{ actionId, actionArgs }: ServerActionAction
): Promise<FetchServerActionResult> {
const body = await encodeReply(actionArgs)
const res = await fetch('', {
method: 'POST',
headers: {
Accept: RSC_CONTENT_TYPE_HEADER,
[ACTION_HEADER]: actionId,
[NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(
JSON.stringify(state.tree)
),
...(process.env.NEXT_DEPLOYMENT_ID
? {
'x-deployment-id': process.env.NEXT_DEPLOYMENT_ID,
}
: {}),
...(nextUrl
? {
[NEXT_URL]: nextUrl,
}
: {}),
},
body,
})
const redirectHeader = res.headers.get('x-action-redirect')
// ... gère la redirection ...
const isPrerender = !!res.headers.get(NEXT_IS_PRERENDER_HEADER)
let revalidatedParts: FetchServerActionResult['revalidatedParts']
// ... gère la revalidation ...
const contentType = res.headers.get('content-type')
// Récupère le contenu retourné par la fonction
if (contentType?.startsWith(RSC_CONTENT_TYPE_HEADER)) {
const response: ActionFlightResponse = await createFromFetch(
Promise.resolve(res),
{ callServer, findSourceMapURL }
)
return {
actionResult: response.a,
actionFlightData: normalizeFlightData(response.f),
redirectLocation,
redirectType,
revalidatedParts,
isPrerender,
}
}
return {
redirectLocation,
redirectType,
revalidatedParts,
isPrerender,
}
}
Cette méthode va venir faire un fetch
API à l'URL qui est actuellement affichée. Il vient rajouter 2 données très importantes :
[ACTION_HEADER]: actionId,
= ceci permet à NextJS de savoir quelle méthode backend appelerAccept: RSC_CONTENT_TYPE_HEADER,
= ceci permet de dire au backend qu'on attend une réponse en format "server-action"
Ensuite, on ajoute le body et d'autres données.
Puis on prend le résultat retourné du fetch
pour reconstruire un gros objet :
return {
actionResult: response.a, // Le résultat retourné de la server-action
redirectLocation, // La redirection si elle existe
redirectType,
revalidatedParts, // La revalidation si elle existe
isPrerender,
};
Et c'est comme ça qu'après, la méthode serverActionReducer
vient récupérer ce gros objet pour venir faire la bonne action en fonction des données disponibles.
Tout ceci, c'est ce qui se passe sous le capot quand tu appelles une server-action.
Comment le backend de NextJS gère les server-actions ?
Sans rentrer dans la technique cette fois, car elle est encore plus compliquée mais le backend de NextJS va venir définir si c'est une server-action via le header [ACTION_HEADER]: actionId
ou pas.
Si c'est une server-action, elle va venir rechercher la bonne action grâce à l'identifiant actionId
et va ensuite appeler la méthode.
Ce qu'on peut voir c'est que cet actionId
est un vrai router :
Juste qu'au lieu d'avoir des URL comme /api/users/1
on vient avoir des méthodes mais en backend, on fait à peu près la même chose.
Conclusion
Je voulais te montrer que les server-actions ne sont pas de la magie et que finalement, comme tout dans la tech (c'est important de se le dire) tout est toujours du code.
Tous ces frameworks nous permettent juste de simplifier la vie des développeurs en offrant une meilleure DX
mais derrière, c'est toujours les mêmes règles qui régissent le monde du JavaScript.
Si tu souhaites vriament maîtriser NextJS je t'invite à t'inscrire à mon cours gratuit ici :
Le meilleur moyen d'apprendre NextJS !
Rejoins par développeurs, cette formation reçoit une note de 4.7 / 5 🚀
Reçois la formation gratuitement dans ta boîte mail :