The Frontastic session

Frontastic provides a session mechanism to its extensions. Any action extensionaction extension - A custom endpoint that can be called. It allows you to forward any kind of API calls to backend services, including writing data to it. can read and write the session, while other extension types can only read the session.

Reading the session works through the property Request.sessionData, a plain JS object of arbitrary (serializable) data. Writing the session can be achieved by setting Response.sessionData.

📘

The Frontastic sessionFrontastic session - The Frontastic session is stored in a cookie on the client side as JWT automatically persists information between Frontastic extension executions. is a mechanism meant for server functions. Frontastic components can't access the session directly (see later in this article). If you only need to conserve data for frontend use, it might be easier to directly use cookies, local storage, or a similar mechanism.

Write the session

The mechanics to use the session in an action schematically works as follows:

someAction: async (request: Request, actionContext: ActionContext): Response => {
  const sessionData = {
    desiredSessionKey: {},
    ...(request.sessionData || {}),
  };


  // ...
  sessionData.desiredSessionKey = 42;
  // ...

  return {
    body: actualActionResult,
    statusCode: 200,
    sessionData,
  } as Response;
},

As a first step, the action code ensures to have properly initialized sessionData. During the execution of the action, the session data is updated (in this case, the value 42 is stored). Eventually, the sessionData is returned as part of the Response.

📘

If you return sessionData from an action, you need to maintain all session information that you didn't touch. If you only return the session key written by the action, all other session data will be lost. Frontastic doesn't perform merge on the session but only stores the returned sessionData.

A complete example looks like this:

export const {
  'actions':
    'star-wars': {
      character: async (request: Request, actionContext: ActionContext): Promise<Response> => {
        const sessionData = {
          moviePreferences: {},
          ...(request.sessionData || {}),
        };

        if (!request.query.search) {
          return {
            body: JSON.stringify([]),
            statusCode: 200,
            sessionData,
          };
        }

        return await axios
          .get('https://swapi.dev/api/people/?search=' + request.query.search)
          .then((response) => {
            if (response.data.results[0]) {
              sessionData.moviePreferences = mergePreferences(
                sessionData.moviePreferences || {},
                calculatePreferences(response.data.results[0].films),
              );
            }

            return {
              body: JSON.stringify(response.data),
              statusCode: 200,
              sessionData,
            } as Response;
          })
          .catch((reason) => {
            return {
              body: reason.body,
              statusCode: 500,
            };
          });
      },
    },
  },
};

This example is an extended version of the action extension example we used in the developing an action extension article. In addition to searching for characters, the action now stores statistics about the Star Wars movie preferences of the user, calculated by looking at the first result of the character search.

📘

If you want to reproduce this complete example, find the code for calculating movie preference statistics at the end of this article.

Read the session

Reading the sessionData is possible in any Frontastic extension since it's part of the Request object. As could already be seen, an action extension receives the Request directly as its input. All other extension types receive the Request as part of their corresponding context data. For example, the following data-source extension can be used to read the tracked moviePreferences:

export default {
  'data-sources': {
    'example/star-wars-movie-preferences': (
      configs: DataSourceConfiguration,
      context: DataSourceContext,
    ): DataSourceResult => {
      console.log('Session data', context.request.sessionData?.moviePreferences);
      return {
        dataSourcePayload: context.request.sessionData?.moviePreferences || {},
      };
    },
  },
};

The DataSourceContext carries the Request, which in turn carries sessionData (which might be empty if no session was written before!).

📘

Direct session access in the frontend code is prohibited because the session might contain sensitive data, such as access tokens. For this reason, the session JWT token is encrypted in production. To use parts of the session in a component, you need to expose this part selectively through a data-source or action as shown in this example.

❗️

Session encryption is a pending feature and hasn't been implemented yet. It'll be delivered soon.

Once this data sourcedata source - The data provided from an API to display products, content, or anything from an integration or extension. A data source filter is created and can be selected for a Frontastic component that will display the data source filter on your commerce site. is registered in Frontastic studioFrontastic studio - The interface you use to manage, build, and edit all areas of your project and commerce sites. Previously known as `backstage`. (see the Developing a data source extension article for more information), a Frontastic componentFrontastic component - A customizable building block that's used together with other components to create a commerce site. Known as `tastic` for short in code. can use the exposed part of the sessionData, for example like this:

import React from 'react';

type FavoriteMovieTasticProps = {
  data: {
    moviePreferences: {
      [episodeNumber: string]: number;
    };
  };
};

const FavoriteMovieTastic: React.FC<FavoriteMovieProps> = ({ data }) => {
  return !data.moviePreferences || data.moviePreferences.dataSource === [] ? (
    <div>Our AI did not receive enough data, yet. Please continue to use our page!</div>
  ) : (
    <div>
      Our AI detected your favorite Star Wars episode is&nbsp;
      <b>
        {
          Object.entries(data.moviePreferences.dataSource).reduce((previousValue, currentValue) => {
            return currentValue[1] > previousValue[1] ? currentValue : previousValue;
          })[0]
        }
      </b>
    </div>
  );
};

export default FavoriteMovieTastic;
{
  "tasticType": "example/star-wars-favorite-movie",
  "name": "Star Wars favorite movie",
  "icon": "star",
  "description": "A Frontastic component showing AI detected favorite movie",
  "schema": [
    {
      "name": "Data source",
      "fields": [
        {
          "label": "Movie preferences",
          "field": "moviePreferences",
          "type": "stream",
          "streamType": "example/star-wars-movie-preferences"
        }
      ]
    }
  ]
}

Movie preference calculation

The following code is used to calculate movie preferences in the action example we used earlier in this article:

export type MoviePreferences = {
  [key: string]: number;
};

export const calculatePreferences = (movieUrls: string[]): MoviePreferences => {
  const preferences: MoviePreferences = {};
  for (const movieUrl of movieUrls) {
    const movieNo = movieUrl.slice(-2, -1);
    if (!preferences[movieNo]) {
      preferences[movieNo] = 0;
    }
    preferences[movieNo]++;
  }
  return preferences;
};

export const mergePreferences = (first: MoviePreferences, second: MoviePreferences = {}): MoviePreferences => {
  const result = { ...first };
  for (const movieKey in second) {
    if (!result[movieKey]) {
      result[movieKey] = 0;
    }
    result[movieKey] += second[movieKey];
  }
  return result;
};

export const getPreferredMovie = (preferences: MoviePreferences): string => {
  return Object.entries(preferences).reduce(
    (previousEntry, currentEntry) => {
      return currentEntry[1] > previousEntry[1] ? currentEntry : previousEntry;
    },
    // Return episode 4 when preferences are empty
    ['4', 0],
  )[0];
};

Did this page help you?