Rupert  Beatty

Rupert Beatty

1658605860

Tenancy: Automatic Multi-tenancy for Laravel. No Code Changes Needed

Tenancy for Laravel — stancl/tenancy

Automatic multi-tenancy for your Laravel app.

You won't have to change a thing in your application's code.

  • ✔️ No model traits to change database connection
  • ✔️ No replacing of Laravel classes (Cache, Storage, ...) with tenancy-aware classes
  • ✔️ Built-in tenant identification based on hostname (including second level domains)

Documentation

Documentation can be found here: https://tenancyforlaravel.com/docs/v3/

The repository with the documentation source code can be found here: stancl/tenancy-docs.

Need help?

Credits

Author: Archtechx
Source Code: https://github.com/archtechx/tenancy 
License: MIT license

#laravel #saas 

Tenancy: Automatic Multi-tenancy for Laravel. No Code Changes Needed
Rupert  Beatty

Rupert Beatty

1658263920

Multi-tenant: Run multiple websites use the same Laravel installation

The unobtrusive Laravel package that makes your app multi tenant. Serving multiple websites, each with one or more hostnames from the same codebase. But with clear separation of assets, database and the ability to override logic per tenant.

Suitable for marketing companies that like to re-use functionality for different clients or start-ups building the next software as a service.


Offers:

  • Integration with the awesome Laravel framework.
  • Event driven, extensible architecture.
  • Close - optional - integration into the web server.
  • The ability to add tenant specific configs, code, routes etc.

Database separation methods:

  • One system database and separated tenant databases (default).
  • Table prefixed in the system database.
  • Or .. manually, the way you want, by listening to an event.

Complete documentation covers more than just the installation and configuration.

Requirements, recommended environment

  • Laravel 8.0+.
  • PHP 7.3+
  • Apache or Nginx.
  • MySQL, MariaDB, or PostgreSQL.

Please read the full requirements in the documentation.

Installation

composer require hyn/multi-tenant

Automatic service registration

Using auto discovery, the tenancy package will be auto detected by Laravel automatically.

Manual service registration

In case you want to disable webserver integration or prefer manual integration, set the dont-discover in your application composer.json, like so:

{
    "extra": {
        "laravel": {
            "dont-discover": [
                "hyn/multi-tenant"
            ]
        }
    }
}

If you disable auto discovery you are able to configure the providers by yourself.

Register the service provider in your config/app.php:

    'providers' => [
        // [..]
        // Hyn multi tenancy.
        Hyn\Tenancy\Providers\TenancyProvider::class,
        // Hyn multi tenancy webserver integration.
        Hyn\Tenancy\Providers\WebserverProvider::class,
    ],

Deploy configuration

First publish the configuration and migration files so you can modify it to your needs:

php artisan vendor:publish --tag tenancy

Open the config/tenancy.php and config/webserver.php file and modify to your needs.

Make sure your system connection has been configured in database.php. In case you didn't override the system connection name the default connection is used.

Now run:

php artisan migrate --database=system

This will run the required system database migrations.


Testing

Run tests using:

vendor/bin/phpunit

If using MySQL, use:

LIMIT_UUID_LENGTH_32=1 vendor/bin/phpunit

Please be warned running tests will reset your current application completely, dropping tenant and system databases and removing the tenancy.json file inside the Laravel directory.

Changes

All changes are covered in the changelog.

Contact

Get in touch personally using;

Testing

Run tests using:

vendor/bin/phpunit

If using MySQL, use:

LIMIT_UUID_LENGTH_32=1 vendor/bin/phpunit

Please be warned running tests will reset your current application completely, dropping tenant and system databases and removing the tenancy.json file inside the Laravel directory.

Changes

All changes are covered in the changelog.

Contact

Get in touch personally using;

Testing

Run tests using:

vendor/bin/phpunit

If using MySQL, use:

LIMIT_UUID_LENGTH_32=1 vendor/bin/phpunit

Please be warned running tests will reset your current application completely, dropping tenant and system databases and removing the tenancy.json file inside the Laravel directory.

Changes

All changes are covered in the changelog.

Contact

Get in touch personally using;

Author: Tenancy
Source Code: https://github.com/tenancy/multi-tenant 
License: MIT license

#laravel #saas #tenant #hacktoberfest 

Multi-tenant: Run multiple websites use the same Laravel installation

SaaS template for AWS, Amplify, React, NextJS and Chakra

AWS + React SaaS Template

End-to-end SaaS Template using AWS Amplify, Apollo Client, Chakra, and NextJS.

 

Table of content

Tech Stack

🔹 DynamoDB
🔹 AppSync (GraphQL)
🔹 Cognito
🔸 React / NextJS
🔸 Amplify
🔸 Apollo Client
🔸 Chakra
▪️ Pulumi
▪️ GitHub Actions

Prerequisites

Create an account on AWS (https://aws.amazon.com/).

You will need to setup the AWS CLI on your local system, if you haven't already.

Create an account on Pulumi (https://pulumi.com/).

You will need to setup the Pulumi CLI and configure it with AWS.

Install Amplify CLI.

We need Amplify to set up the Front End, so you need to setup the Amplify CLI on your local system.

ℹ️ Additional info

This repository is a monorepo, but you can split out the front-end and back-end folders into separate repositories.

Setup

Setup the stack in the cloud

Go to the back-end folder:
cd back-end

Install the dependencies:
npm install

Use Pulumi to setup the stack in the cloud:
pulumi up

This command will set up a stack consisting of 18 resources in AWS.
They boil down to:

  • A DynamoDB table for users
  • A Cognito user pool
  • An Identity Pool
  • AppSync GraphQL API
  • A PostConfirmation Lambda which is triggered when a user signs up
  • A Resolver Lambda which is used to fetch a user through GraphQL

When the pulumi up command has finished running, you will get an output that looks similar to this.
Note down this output:

Outputs:
    appSyncID       : "<APPSYNC-ID>"
    dynamoID        : "<TABLE-NAME>"
    graphQLEndpoint : {
        GRAPHQL : "https://<GRAPHQL-ENDPOINT>/graphql"
        REALTIME: "wss://<REALTIME-ENDPOINT>/graphql"
    }
    identityPoolID  : "<IDENTITY-POOL-ID>"
    userpoolClientID: "<USERPOOL-CLIENT-ID>"
    userpoolID      : "<USERPOOL-ID>"

ℹ️ Additional info

If Pulumi complains about missing region, use the command: pulumi config set aws:region eu-west-1

If you want to use another region than eu-west-1, go to the file back-end/resources/variables/ and change the region here as well.

export const variables = {
  region: 'eu-west-1' as const, // <-- change this to your region
  dynamoDBTables: {} as Record<string, Output<string>>,
};

Setup the front-end

Go to the front-end folder:
cd front-end

Install the dependencies:
npm install

Use Amplify to link the front-end to the back-end: amplify init

  • ? Enter a name for the project: my-app-name
  • ? Choose the environment you would like to use: dev
  • ? Choose your default editor: Visual Studio Code
  • ? Choose the type of app that you're building: javascript
  • ? What javascript framework are you using: react
  • ? Source Directory Path: src
  • ? Distribution Directory Path: build
  • ? Build Command: npm run build
  • ? Start Command: npm run start
  • ? Select the authentication method you want to use: AWS profile
  • ? Please choose the profile you want to use: <default - or pick the one you use>

This will setup an Amplify project in the cloud for the front-end.

A file called src/aws-exports.js will be created. You can safely deleted this file.
(In fact, you can delete the src folder entirely).

Configure the environment

Go to the file front-end/deployment/config/config-development.ts.
Now, use the Pulumi output from before, to setup the environment:

const configDevelopment = {
  ...

  /**
   * Add the details from the Pulumi output here, after running 'pulumi up'
   */
  USER_POOL_CLIENT_ID: '<USERPOOL-CLIENT-ID>',
  USER_POOL_ID: '<USERPOOL-ID>',
  IDENTITY_POOL_ID: '<IDENTITY-POOL-ID>',
  GRAPHQL_ENDPOINT: 'https://<GRAPHQL-ENDPOINT>/graphql',
};

Next, run the command: amplify codegen add -apiId <APPSYNC-ID>

  • ? Choose the code generation language target: typescript
  • ? Enter the file name pattern of graphql queries, mutations and subscriptions: /graphql/**/*.ts
  • ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions? Y
  • ? Enter maximum statement depth [increase from default if your schema is deeply nested]: 4
  • ? Enter the file name for the generated code: /graphql/API.ts
  • ? Do you want to generate code for your newly created GraphQL API? Y

Add hosting

Finally, setup hosting for the front-end:
amplify hosting add

  • ? Select the plugin module to execute: Hosting with Amplify Console
  • ? Choose a type: Continuous deployment

Sign into AWS and link your GitHub repository to Amplify Console.
When the environment has been set up, go to the Environment variables page from the left menu, and click Manage variables.

Add a new variable called NEXT_PUBLIC_CLOUD_ENV with the value dev.

Amplify will create a hosted URL for you.
Go to the file front-end/deployment/config/config-development.ts again, and add the URL here:

/**
 * Add your hosted dev URL here
 */
const HOSTED_URL = '<ADD-YOUR-HOSTED-URL-HERE>';

Finally, run:
amplify push
amplify publish

Let Amplify do it's thing 😎

Start the app locally

Start the app by running npm run dev.
The app will start locally on http://localhost:3000.

Create a new user by going to the /signup page.
Sign into the app by going to the /signin page.

Authentication should work smoothly at this point - now, start building your SaaS 🚀

ℹ️ Additional info

If you want to use different environments (dev and prod), simply set up Pulumi in a different environment, and paste the output into the front-end/deployment/config/config-production.ts.

Similarly, create an environment variable in the Amplify Console for the production environment called NEXT_PUBLIC_CLOUD_ENV with the value prod.

Get Help

Contribute

PRs are welcome!


Author: SimonHoiberg
Source code: https://github.com/SimonHoiberg/saas-template
License: MIT license

#react-native #react #aws #saas #nextjs 

SaaS template for AWS, Amplify, React, NextJS and Chakra

La Diferencia Entre IPaaS Y ESB

Según un informe de IDC , se espera que las inversiones en servicios de nube pública crezcan a 203 400 millones de dólares en todo el mundo, con una CAGR del 21,5 %. La abrumadora velocidad a la que las empresas están adoptando servicios en la nube está creando un desafío de integración para las empresas que utilizan sistemas de integración locales más antiguos, como los ESB, cuyos sistemas no fueron diseñados para manejar la integración en la nube.

Como respuesta, las empresas están recurriendo a herramientas y soluciones de  integración como la plataforma de integración como servicio (iPaaS) para complementar las debilidades de sus instalaciones de  bus de servicios empresariales (ESB) existentes.

Pero, ¿cuáles son exactamente las diferencias entre iPaaS y ESB? ¿Cuándo debería una empresa usar iPaaS? ¿Qué hay de ESB? En esta publicación, arrojamos algo de luz sobre las diferencias entre estas dos herramientas de integración.

iPaaS frente a ESB: una descripción general

iPaaS y ESB tienen el mismo propósito principal: la integración de sistemas y aplicaciones empresariales. La principal diferencia entre iPaaS y ESB radica en el tipo de sistemas que integran mejor, el nivel de complejidad de sus integraciones y su escalabilidad.

Tipo de sistema

Generalmente, iPaaS y ESB están en extremos opuestos del continuo. iPaaS es un conjunto de herramientas de integración proporcionadas desde una nube pública y no requiere hardware o software local. iPaaS fue diseñado específicamente para manejar los estándares de documentos y mensajes livianos (REST, JSON, etc.) que utilizan las aplicaciones en la nube de hoy.

ESB, por otro lado, es un modelo de arquitectura de software local que generalmente utiliza tecnología común antes del surgimiento de la nube. Como tal, su gran huella local y los estándares de documentos y mensajes más antiguos funcionan mejor para integrar sistemas locales y agregados como SAP.

Sin embargo, estos dos modelos de integración están viendo venir la convergencia; algunas soluciones iPaaS han evolucionado para admitir sistemas locales, mientras que algunos proveedores de ESB han introducido funciones para admitir de manera más elegante la integración de servicios en la nube.

Complejidad

ESB está diseñado para integrar arquitectura y sistemas de TI complejos. Mantiene unidos los sistemas locales y heredados de una empresa. iPaaS, por otro lado, ofrece una solución de integración más liviana y más adecuada para aplicaciones flexibles y en tiempo real, que son requisitos críticos de los servicios basados ​​en la nube.

Escalabilidad

Finalmente, un punto importante de diferencia entre iPaaS y ESB es la dirección de la escalabilidad. ESB es el más adecuado para la escalabilidad vertical: la integración de la arquitectura y los sistemas internos complejos de una empresa. Esto tiene sentido si se considera la evolución del ESB, que llegó a la mayoría de edad hace más de una década, cuando los sistemas de planificación de recursos empresariales (ERP) eran los pesos pesados ​​del software empresarial. Por otro lado, iPaaS es más adecuado para la escalabilidad horizontal: integración con terceros, socios y aplicaciones ad hoc, como soluciones de software como servicio (SaaS). Ligero y flexible, iPaaS permite a las empresas conectarse e integrar rápidamente sus aplicaciones y sistemas en la nube.

Profundizando: otras diferencias y casos de uso de iPaaS

Aparte de estas diferencias principales, hay otros factores que diferencian iPaas y ESB, como la multitenencia. Comprender los casos de uso de iPaaS también puede ayudar a contrastar aún más las dos tecnologías.

Arquitectura multiusuario

Una arquitectura de múltiples inquilinos se refiere a una sola instancia de software que se ejecuta en un solo servidor mientras atiende a múltiples grupos de usuarios. Por ejemplo, un único software que atiende a diferentes personas o departamentos, como finanzas, marketing y operaciones, opera en una arquitectura de múltiples inquilinos. Debido a su complejidad inherente, la mayoría de las soluciones ESB no son multiusuario.

Por otro lado, iPaaS admite multiusuario. Esto le da a las soluciones iPaaS una ventaja sobre ESB, ya que múltiples inquilinos o usuarios pueden compartir una sola instancia para reducir efectivamente las redundancias en los procesos de integración. La tenencia múltiple también puede reducir los costos administrativos y de infraestructura durante la integración.

Integración ad hoc

Tradicionalmente, las iniciativas de software y aplicaciones las lleva a cabo el departamento de TI de una empresa. Sin embargo, cada vez más departamentos están autorizados para comprar, instalar y utilizar su propio software de forma ad hoc, especialmente ahora que las soluciones SaaS hacen posible que las aplicaciones se incluyan en los gastos operativos de un departamento en lugar de sus gastos de capital. Las soluciones ad hoc requieren una integración flexible, ligera y en tiempo real.

ESB es demasiado lento y complejo para proyectos de integración ad hoc. Requiere la experiencia de los departamentos de TI, lo que anula el propósito de las iniciativas de aplicaciones ad hoc y ralentiza la entrega de proyectos. La simplicidad, la flexibilidad y las capacidades en tiempo real de las soluciones iPaaS ayudan a satisfacer las demandas aceleradas de las integraciones ad hoc y ayudan a otros departamentos auxiliares a lograr sus objetivos comerciales de manera eficiente sin verse frenados por la complejidad de ESB.

Integración SaaS

Hoy, tanto iPaaS como ESB pueden integrar soluciones SaaS con sistemas heredados y locales. iPaaS utiliza inherentemente conectores ligeros como JSON y API, que son los más adecuados para la integración de SaaS, mientras que hay un número creciente de soluciones ESB que pueden utilizar los mismos protocolos de servicios web ligeros. Los expertos llaman a estas tecnologías de integración ESB ligeros o ESB en la nube. Sin embargo, el ESB tradicional utiliza tecnologías de middleware más complejas, como la arquitectura de middleware orientado a mensajes (MOM), que no utilizan las soluciones SaaS.

Integración IoT

Además de SaaS, otra tendencia emergente es el auge de los dispositivos habilitados para Internet o Internet de las cosas (IoT). La integración de IoT requiere una escalabilidad horizontal significativamente alta debido al gran volumen de dispositivos conectados, la conectividad liviana y la baja latencia requerida para un rendimiento óptimo. Además de estos requisitos, la integración de IoT también exige una conexión en tiempo real. Estos factores hacen que ESB no sea adecuado para este tipo de integración. Además, la integración de IoT también requiere integración externa. Juntando todos estos factores, es fácil concluir que la mejor solución de integración para IoT es iPaaS.

Tecnologías complementarias, no competidoras

A pesar de sus aparentemente abrumadoras ventajas sobre ESB, iPaaS todavía tiene sus limitaciones. Todavía no es práctico ni rentable utilizar iPaaS para empresas con sistemas organizativos complejos y arquitecturas internas. ESB sigue siendo el pegamento preferido para mantener unidos los sistemas internos. Por esta razón, las empresas suelen utilizar iPaaS y ESB al mismo tiempo para mantener juntos su arquitectura interna y sus sistemas heredados, al tiempo que se adaptan a nuevos puntos finales de integración, como SaaS, servicios en la nube y dispositivos IoT.

Fuente del artículo original en: https://dzone.com/articles/ipaas-vs-esb-understanding-the-difference

#ipaas  #esb #saas #iot #applications #webservice 

La Diferencia Entre IPaaS Y ESB

iPaaSとESBの違いは正確には何ですか?

IDCのレポートによると、パブリッククラウドサービスへの投資は、21.5%のCAGRで、世界全体で2,034億ドルに成長すると予想されています。企業がクラウドサービスを採用している圧倒的な速度は、システムがクラウド統合を処理するように設計されていないESBなどの古いオンプレミス統合システムを使用している企業にとって統合の課題を生み出しています。

 その対応として、企業は、既存のエンタープライズサービスバス(ESB)インストールの弱点を補うために 、Integration Platform as a Service(iPaaS)などの統合ツールやソリューションに目を向けています。

しかし、iPaaSとESBの違いは正確には何ですか?企業はいつiPaaSを使用する必要がありますか?ESBはどうですか?この投稿では、これら2つの統合ツールの違いに光を当てます。

iPaaSとESB:概要

iPaaSとESBは、エンタープライズシステムとアプリケーションの統合という同じ主な目的を果たします。iPaaSとESBの主な違いは、それらが最もよく統合するシステムの種類、それらの統合の複雑さのレベル、およびそれらのスケーラビリティにあります。

システムタイプ

一般的に、iPaaSとESBは連続体の反対側にあります。iPaaSは、パブリッククラウドから提供される統合ツールのセットであり、オンプレミスのハードウェアやソフトウェアを必要としません。iPaaSは、今日のクラウドアプリで使用されている軽量のメッセージングおよびドキュメント標準(REST、JSONなど)を処理するように特別に設計されています。

一方、ESBはオンプレミスのソフトウェアアーキテクチャモデルであり、通常、クラウドが登場する前に一般的なテクノロジーを利用します。そのため、オンプレミスのフットプリントが大きく、古いメッセージングおよびドキュメント標準は、オンプレミスとSAPなどの集約システムを統合するのに最適です。

ただし、これら2つの統合モデルは収束しつつあります。一部のiPaaSソリューションは、オンプレミスシステムをサポートするように進化しましたが、一部のESBベンダーは、クラウドサービスの統合をよりエレガントにサポートする機能を導入しました。

複雑

ESBは、複雑なITシステムとアーキテクチャを統合するように設計されています。これは、企業のオンプレミスシステムとレガシーシステムをまとめたものです。一方、iPaaSは、クラウドベースのサービスの重要な要件である柔軟でリアルタイムのアプリケーションにより適した、より軽量な統合ソリューションを提供します。

スケーラビリティ

最後に、iPaaSとESBの主な違いは、スケーラビリティの方向性です。ESBは、企業の複雑な内部システムとアーキテクチャの統合である垂直スケーラビリティに最適です。これは、エンタープライズリソースプランニング(ERP)システムがエンタープライズソフトウェアの大物であった10年以上前に成熟したESBの進化を考えると理にかなっています。一方、iPaaSは、サードパーティ、パートナー、およびSoftware as a Service(SaaS)ソリューションなどのアドホックアプリケーションとの統合という、水平方向のスケーラビリティに適しています。軽量で柔軟性のあるiPaaSにより、企業はクラウドアプリケーションとシステムをすばやく接続して統合できます。

さらに深く:iPaaSの他の違いとユースケース

これらの主な違いの他に、マルチテナンシーなど、iPaasとESBを区別する他の要因があります。iPaaSのユースケースを理解することも、2つのテクノロジーをさらに対比するのに役立ちます。

マルチテナントアーキテクチャ

マルチテナントアーキテクチャとは、複数のユーザーグループにサービスを提供しながら、単一のサーバーで実行されるソフトウェアの単一のインスタンスを指します。たとえば、財務、マーケティング、運用など、さまざまな個人や部門に対応する単一のソフトウェアは、マルチテナントアーキテクチャで動作します。固有の複雑さのため、ほとんどのESBソリューションはマルチテナントではありません。

一方、iPaaSはマルチテナンシーをサポートしています。これにより、iPaaSソリューションはESBよりも有利になります。これは、複数のテナントまたはユーザーが単一のインスタンスを共有して、統合プロセスの冗長性を効果的に削減できるためです。マルチテナンシーは、統合中のインフラストラクチャと管理コストも削減できます。

アドホック統合

従来、ソフトウェアとアプリケーションのイニシアチブは、企業のIT部門によって実行されていました。ただし、今日、ますます多くの部門がアドホックベースで独自のソフトウェアを購入、インストール、および利用できるようになっています。特に、SaaSソリューションにより、アプリケーションが資本的支出ではなく部門の営業費用に分類されるようになっています。アドホックソリューションには、柔軟で軽量なリアルタイムの統合が必要です。

ESBは、アドホック統合プロジェクトには遅すぎて複雑です。IT部門の専門知識が必要であり、アドホックアプリケーションイニシアチブの目的を無効にし、プロジェクトの提供を遅らせます。iPaaSソリューションのシンプルさ、柔軟性、およびリアルタイム機能は、アドホック統合のペースの速い要求に対応し、他の補助部門がESBの複雑さにとらわれることなくビジネス目標を効率的に達成するのに役立ちます。

SaaS統合

現在、iPaaSとESBはどちらも、SaaSソリューションをオンプレミスおよびレガシーシステムと統合できます。iPaaSは本質的にSaaS統合に最適なJSONやAPIなどの軽量コネクタを使用しますが、同じ軽量Webサービスプロトコルを利用できるESBソリューションの数は増え続けています。専門家は、これらの統合テクノロジーを軽量ESBまたはクラウドESBと呼んでいます。ただし、従来のESBは、SaaSソリューションでは使用されないメッセージ指向ミドルウェア(MOM)アーキテクチャなどのより複雑なミドルウェアテクノロジを使用します。

IoT統合

SaaSに加えて、もう1つの新しいトレンドは、インターネット対応デバイスまたはモノのインターネット(IoT)の台頭です。IoT統合には、接続されたデバイスの膨大な量、軽量の接続、および最適なパフォーマンスに必要な低遅延のために、非常に高い水平スケーラビリティが必要です。これらの要件とは別に、IoT統合にはリアルタイム接続も必要です。これらの要因により、ESBはこの種の統合にはあまり適していません。さらに、IoT統合には外部統合も必要です。これらすべての要素を総合すると、IoTのより優れた統合ソリューションはiPaaSであると簡単に結論付けることができます。

補完する、競合しないテクノロジー

ESBに比べて一見圧倒的な利点があるように見えますが、iPaaSにはまだ制限があります。複雑な組織システムと内部アーキテクチャを備えた企業にiPaaSを利用することは、まだ実用的でなく、費用対効果も高くありません。ESBは、内部システムをまとめるための好ましい接着剤です。このため、iPaaSとESBは、SaaS、クラウドサービス、IoTデバイスなどの新しい統合エンドポイントに対応しながら、内部アーキテクチャとレガシーシステムをまとめるために企業によって同時に利用されることがよくあります。

元の記事のソース:https ://dzone.com/articles/ipaas-vs-esb-understanding-the-difference

#ipaas  #esb #saas #iot #applications #webservice 

iPaaSとESBの違いは正確には何ですか?

Why Laravel Framework is Superior for Mobile App Development?

To build a world-class mobile app, you need a robust and trustworthy Laravel Framework. It is your chosen technology that keeps your data safe and constantly checks with the smooth running of backend operations. Laravel got founded with the same thought process. The creators of Laravel technology believe in simplifying the development process. With the latest version of Laravel 5.4, the founders have reinforced the same ideology embedded with new features.

https://brainstreamtechnolabs.com/reasons-why-laravel-framework-is-superior-for-mobile-app-development/

#laravelframework  #php #backend #technologies #newblog  #MobileAppDevelopment #newarticle #softwareasaservice #saas #softwaredeveloper #ecommercewebsite #fullstackdeveloper #softwareengineer 

Why Laravel Framework is Superior for Mobile App Development?
藤本  結衣

藤本 結衣

1652117100

NextJS / React SSR:21のユニバーサルデータフェッチパターンとベストプラクティス

フロントエンド開発者は、データが実際にフロントエンドに入る方法を気にすることなく、特定のページに必要なデータを定義できる必要があります。

それは私の友人が最近の議論で言ったことです。NextJSでユニバーサルデータフェッチを行う簡単な方法がないのはなぜですか?

この質問に答えるために、NextJSでのユニバーサルデータフェッチに関連する課題を見てみましょう。しかし、最初に、ユニバーサルデータフェッチとは実際には何ですか?

免責事項:これは長くて詳細な記事になります。それは多くの分野をカバーし、細部にかなり深く入り込むでしょう。あなたが軽量のマーケティングブログを期待しているなら、この記事はあなたのためではありません。

NextJSユニバーサルデータフェッチ

私のユニバーサルデータフェッチの定義は、アプリケーションのどこにでもデータフェッチフックを配置でき、それが機能するということです。このデータフェッチフックは、追加の構成なしでアプリケーションのどこでも機能するはずです。

これはおそらく最も複雑な例ですが、私はそれをあなたと共有しないことに興奮しています。

これは「ユニバーサルサブスクリプション」フックです。

const PriceUpdates = () => {
    const data = useSubscription.PriceUpdates();
    return (
        <div>
            <h1>Universal Subscription</h1>
            <p>{JSON.stringify(data)}</p>
        </div>
    )
}

「PriceUpdates」フックは、プロジェクトで「PriceUpdates.graphql」ファイルを定義したため、フレームワークによって生成されます。

このフックの何が特別なのですか?ReactComponentはアプリケーションのどこにでも自由に配置できます。デフォルトでは、サブスクリプションの最初のアイテムをサーバーレンダリングします。サーバーでレンダリングされたHTMLは、データとともにクライアントに送信されます。クライアントはアプリケーションを再水和し、サブスクリプション自体を開始します。

これはすべて、追加の構成なしで実行されます。これはアプリケーションのどこでも機能するため、ユニバーサルデータフェッチという名前が付けられています。GraphQLオペレーションを記述して、必要なデータを定義すると、フレームワークが残りの処理を行います。

ネットワーク呼び出しが行われているという事実を隠そうとしているのではないことに注意してください。ここで行っているのは、フロントエンド開発者に生産性を還元することです。データのフェッチ方法、APIレイヤーの保護方法、使用するトランスポートなどについて心配する必要はありません。問題なく機能するはずです。

NextJSでのデータフェッチが非常に難しいのはなぜですか?

NextJSをしばらく使用している場合、データのフェッチに関して正確に何が難しいのかと疑問に思われるかもしれません。

NextJSでは、「/ api」ディレクトリにエンドポイントを定義するだけで、「swr」または単に「fetch」を使用して呼び出すことができます。

「Hello、world!」というのは正しいです。「/api」からデータをフェッチする例は非常に簡単ですが、アプリケーションを最初のページを超えてスケ​​ーリングすると、開発者をすぐに圧倒する可能性があります。

NextJSでのデータフェッチの主な課題を見てみましょう。

getServerSidePropsはルートページでのみ機能します

デフォルトでは、非同期関数を使用してサーバー側のレンダリングに必要なデータをロードできる唯一の場所は、各ページのルートです。

NextJSドキュメントの例を次に示します。

function Page({ data }) {
  // Render data...
}


// This gets called on every request
export async function getServerSideProps() {
  // Fetch data from external API
  const res = await fetch(`https://.../data`)
  const data = await res.json()


  // Pass data to the page via props
  return { props: { data } }
}


export default Page

何百ものページとコンポーネントがあるWebサイトを想像してみてください。各ページのルートですべてのデータ依存関係を定義する必要がある場合、コンポーネントツリーをレンダリングする前に、実際に必要なデータをどのように知ることができますか?ルートコンポーネント用にロードしたデータによっては、一部のロジックが子コンポーネントを完全に変更することを決定する場合があります。

大規模なNextJSアプリケーションを保守する必要がある開発者と話をしました。彼らは、「getServerSideProps」でのデータのフェッチは、多数のページやコンポーネントでは適切に拡張できないと明確に述べています。

認証により、データフェッチがさらに複雑になります

ほとんどのアプリケーションには、ある種の認証メカニズムがあります。公開されているコンテンツがあるかもしれませんが、Webサイトをパーソナライズしたい場合はどうでしょうか。

ユーザーごとに異なるコンテンツをレンダリングする必要があります。

クライアントでのみユーザー固有のコンテンツをレンダリングする場合、データが入ってくると、この醜い「ちらつき」効果に気づきましたか?

クライアントでユーザー固有のコンテンツのみをレンダリングしている場合は、準備が整うまでページが複数回再レンダリングされるという効果が常に得られます。

理想的には、データフェッチフックはすぐに認証を認識します。

バグを回避し、開発者の生産性を高めるには、型安全性が必要です

上記の例で「getServerSideProps」を使用して見たように、APIレイヤーをタイプセーフにするために追加のアクションを実行する必要があります。データフェッチフックがデフォルトでタイプセーフであるとしたら、もっと良いと思いませんか?

サブスクリプションはサーバー上でレンダリングできませんね。

これまでのところ、NextJSでサーバー側のレンダリングをサブスクリプションに適用した人を見たことがありません。しかし、SEOとパフォーマンスの理由で株価をサーバーレンダリングしたいが、更新を受信するためにクライアント側のサブスクリプションも必要な場合はどうでしょうか。

確かに、サーバーでQuery / GETリクエストを使用してから、クライアントでサブスクリプションを追加することもできますが、これにより多くの複雑さが増します。もっと簡単な方法があるはずです!

ユーザーがウィンドウを離れて再びウィンドウに入るとどうなりますか?

出てくるもう1つの質問は、ユーザーがウィンドウを離れて再びウィンドウに入るとどうなるかということです。サブスクリプションを停止する必要がありますか、それともデータのストリーミングを継続する必要がありますか?ユースケースとアプリケーションの種類によっては、予想されるユーザーエクスペリエンスと取得するデータの種類に応じて、この動作を微調整することをお勧めします。データフェッチフックはこれを処理できるはずです。

突然変異は他のデータフェッチフックに影響を与える必要がありますか?

突然変異が他のデータフェッチフックに副作用をもたらすことは非常に一般的です。たとえば、タスクのリストを作成できます。新しいタスクを追加するときは、タスクのリストも更新する必要があります。したがって、データフェッチフックはこのような状況を処理できる必要があります。

遅延読み込みはどうですか?

もう1つの一般的なパターンは、遅延読み込みです。ユーザーがページの一番下までスクロールしたときや、ユーザーがボタンをクリックしたときなど、特定の条件下でのみデータをロードしたい場合があります。このような場合、データフェッチフックは、データが実際に必要になるまでフェッチの実行を延期できる必要があります。

ユーザーが検索語を入力したときに、クエリの実行をどのようにデバウンスできますか?

データフェッチフックのもう1つの重要な要件は、クエリの実行をデバウンスすることです。これは、サーバーへの不要な要求を回避するためです。ユーザーが検索ボックスに検索語を入力している状況を想像してみてください。ユーザーが文字を入力するたびに、本当にサーバーにリクエストを送信する必要がありますか?デバウンスを使用してこれを回避し、データフェッチフックのパフォーマンスを向上させる方法を見ていきます。

NextJSのデータフェッチフックを構築する際の最大の課題の概要

  1. getServerSidePropsはルートページでのみ機能します
  2. 認証対応のデータフェッチフック
  3. 型安全性
  4. サブスクリプションとSSR
  5. ウィンドウフォーカス&ブラー
  6. 突然変異の副作用
  7. 遅延読み込み
  8. デバウンス

それは私たちが解決する必要がある8つのコア問題に私たちをもたらします。次に、これらの問題を解決する21のパターンとベストプラクティスについて説明します。

21パターンとベストプラクティスNextJSのデータフェッチフックのコア8コア問題を解決する

これらのパターンを自分で追跡して体験したい場合は、このリポジトリのクローンを作成して試してみることができます。パターンごとに、デモには専用のページがあります

デモを開始したら、ブラウザを開いて、でパターンの概要を確認できますhttp://localhost:3000/patterns

GraphQLを使用してデータフェッチフックを定義していることに気付くでしょうが、実装は実際にはGraphQL固有ではありません。同じパターンをRESTなどの他のAPIスタイル、またはカスタムAPIでも適用できます。

1.クライアント側のユーザー

最初に確認するパターンはクライアント側のユーザーです。これは、認証対応のデータフェッチフックを構築するための基盤です。

現在のユーザーを取得するためのフックは次のとおりです。

useEffect(() => {
        if (disableFetchUserClientSide) {
            return;
        }
        const abort = new AbortController();
        if (user === null) {
            (async () => {
                try {
                    const nextUser = await ctx.client.fetchUser(abort.signal);
                    if (JSON.stringify(nextUser) === JSON.stringify(user)) {
                        return;
                    }
                    setUser(nextUser);
                } catch (e) {
                }
            })();
        }
        return () => {
            abort.abort();
        };
    }, [disableFetchUserClientSide]);

ページルート内で、このフックを使用して現在のユーザーをフェッチします(サーバーでまだフェッチされていない場合)。アボートコントローラを常にクライアントに渡すことが重要です。そうしないと、メモリリークが発生する可能性があります。フックを含むコンポーネントがマウント解除されると、戻り矢印関数が呼び出されます。

潜在的なメモリリークを適切に処理するために、アプリケーション全体でこのパターンを使用していることに気付くでしょう。

次に、「client.fetchUser」の実装を調べてみましょう。

public fetchUser = async (abortSignal?: AbortSignal, revalidate?: boolean): Promise<User<Role> | null> => {
    try {
        const revalidateTrailer = revalidate === undefined ? "" : "?revalidate=true";
        const response = await fetch(this.baseURL + "/" + this.applicationPath + "/auth/cookie/user" + revalidateTrailer, {
            headers: {
                ...this.extraHeaders,
                "Content-Type": "application/json",
                "WG-SDK-Version": this.sdkVersion,
            },
            method: "GET",
            credentials: "include",
            mode: "cors",
            signal: abortSignal,
        });
        if (response.status === 200) {
            return response.json();
        }
    } catch {
    }
    return null;
};

クライアントのクレデンシャルやトークンなどは送信されていないことに気付くでしょう。サーバーによって設定された、クライアントがアクセスできない、安全で暗号化されたhttpのみのCookieを暗黙的に送信します。

知らない人のために、あなたが同じドメインにいる場合、httpのみのCookieが各リクエストに自動的に添付されます。HTTP / 2を使用している場合は、クライアントとサーバーがヘッダー圧縮を適用することもできます。つまり、クライアントとサーバーの両方が既知のヘッダーキー値のマップをネゴシエートできるため、すべてのリクエストでCookieを送信する必要はありません。接続レベルのペア。

認証を簡単にするために舞台裏で使用しているパターンは、「トークンハンドラーパターン」と呼ばれます。トークンハンドラーパターンは、最新のJavaScriptアプリケーションで認証を処理するための最も安全な方法です。非常に安全ですが、IDプロバイダーにとらわれないようにすることもできます。

トークンハンドラーパターンを適用することで、異なるIDプロバイダーを簡単に切り替えることができます。これは、「バックエンド」がOpenIDConnect依存パーティとして機能しているためです。

あなたが尋ねるかもしれない依拠党とは何ですか?これは、認証をサードパーティにアウトソーシングするOpenIDConnectクライアントを備えたアプリケーションです。OpenID Connectのコンテキストで話しているように、「バックエンド」はOpenIDConnectプロトコルを実装するすべてのサービスと互換性があります。このように、バックエンドはシームレスな認証エクスペリエンスを提供でき、開発者はKeycloak、Auth0、Okta、PingIdentityなどのさまざまなIDプロバイダーから選択できます...

ユーザーの観点から、認証フローはどのように見えますか?

  1. ユーザーがログインをクリックします
  2. フロントエンドはユーザーをバックエンド(依存側)にリダイレクトします
  3. バックエンドはユーザーをIDプロバイダーにリダイレクトします
  4. ユーザーはIDプロバイダーで認証します
  5. 認証が成功すると、IDプロバイダーはユーザーをバックエンドにリダイレクトします
  6. 次に、バックエンドは認証コードをアクセスおよびIDトークンと交換します
  7. アクセストークンとIDトークンは、クライアントに安全で暗号化されたhttpのみのCookieを設定するために使用されます
  8. Cookieを設定すると、ユーザーはフロントエンドにリダイレクトされます

今後、クライアントがメソッドを呼び出すとfetchUser、Cookieがバックエンドに自動的に送信されます。このように、フロントエンドはログイン中は常にユーザーの情報にアクセスできます。

ユーザーがログアウトをクリックすると、Cookieを無効にする関数がバックエンドで呼び出されます。

これはすべて消化するのが難しいかもしれないので、重要な部分を要約しましょう。まず、バックエンドがReyling Partyとして機能できるように、どのIDプロバイダーと連携するかをバックエンドに指示する必要があります。これが完了すると、フロントエンドから認証フローを開始し、バックエンドから現在のユーザーをフェッチして、ログアウトすることができます。

この「fetchUser」呼び出しをuseEffect各ページのルートに配置するフックにラップする場合、現在のユーザーが何であるかを常に知ることができます。

ただし、落とし穴があります。デモを開いてクライアント側のユーザーページに移動すると、ページが読み込まれた後にちらつきの効果があることに気付くでしょう。これは、fetchUser呼び出しがクライアントで行われているためです。

Chrome DevToolsを見てページのプレビューを開くと、ユーザーオブジェクトがに設定された状態でページがレンダリングされていることがわかりますnull。ログインボタンをクリックすると、ログインフローを開始できます。完了したら、ページを更新すると、ちらつき効果が表示されます。

トークンハンドラーパターンの背後にあるメカニズムを理解したので、最初のページの読み込み時にちらつきを取り除く方法を見てみましょう。

2.サーバー側ユーザー

ちらつきを取り除きたい場合は、サーバー側のレンダリングを適用できるように、サーバー側でユーザーをロードする必要があります。同時に、サーバー側でレンダリングされたユーザーをクライアントに誘導する必要があります。その2番目のステップを見逃すと、サーバーでレンダリングされたhtmlが最初のクライアント側のレンダリングと異なるため、クライアントの再ハイドレーションは失敗します。

では、サーバー側のユーザーオブジェクトにアクセスするにはどうすればよいでしょうか。私たちが持っているのは、ドメインに添付されたCookieだけであることを忘れないでください。

たとえば、バックエンドがで実行されてapi.example.comおり、フロントエンドがwww.example.comまたはで実行されているとしexample.comます。

Cookieについて知っておくべき重要なことが1つあるとすれば、サブドメインを使用している場合は、親ドメインにCookieを設定できるということです。つまり、認証フローが完了すると、バックエンドはapi.example.comドメインにCookieを設定しないようにする必要があります。代わりに、Cookieをexample.comドメインに設定する必要があります。そうすることで、Cookieは、、およびそれ自体example.comを含むwww.example.com、のすべてのサブドメインに表示されるようになります。api.example.comexample.com

ちなみに、これはシングルサインオンを実装するための優れたパターンです。ユーザーに一度ログインしてもらい、すべてのサブドメインで認証されます。

バックエンドがサブドメイン上にある場合、WunderGraphは自動的にCookieを親ドメインに設定するため、これについて心配する必要はありません。

ここで、サーバー側のユーザーの取得に戻ります。サーバー側でユーザーを取得するにはgetInitialProps、ページのメソッドにいくつかのロジックを実装する必要があります。

WunderGraphPage.getInitialProps = async (ctx: NextPageContext) => {


// ... omitted for brevity


const cookieHeader = ctx.req?.headers.cookie;
if (typeof cookieHeader === "string") {
    defaultContextProperties.client.setExtraHeaders({
        Cookie: cookieHeader,
    });
}


let ssrUser: User<Role> | null = null;


if (options?.disableFetchUserServerSide !== true) {
    try {
        ssrUser = await defaultContextProperties.client.fetchUser();
    } catch (e) {
    }
}


// ... omitted for brevity
return {...pageProps, ssrCache, user: ssrUser};

関数のctxオブジェクトには、getInitialPropsヘッダーを含むクライアント要求が含まれています。サーバー側で作成した「APIクライアント」がユーザーに代わって動作できるように、「魔法のトリック」を実行できます。

フロントエンドとバックエンドの両方が同じ親ドメインを共有しているため、バックエンドによって設定されたCookieにアクセスできます。したがって、Cookieヘッダーを取得してAPIクライアントのCookieヘッダーとして設定すると、APIクライアントは、サーバー側でもユーザーのコンテキストで動作できるようになります。

これで、サーバー側でユーザーをフェッチし、pagePropsと一緒にユーザーオブジェクトをページのレンダリング関数に渡すことができます。この最後のステップを見逃さないようにしてください。そうしないと、クライアントの再水和が失敗します。

了解しました。少なくとも更新を押したときのちらつきの問題は解決しました。しかし、別のページから始めて、クライアント側のナビゲーションを使用してこのページにアクセスした場合はどうなるでしょうか。

デモを開いて、自分で試してみてください。nullユーザーが他のページにロードされていない場合、ユーザーオブジェクトがに設定されることがわかります。

この問題も解決するには、さらに一歩進んで「ユニバーサルユーザー」パターンを適用する必要があります。

3.ユニバーサルユーザー

ユニバーサルユーザーパターンは、前の2つのパターンの組み合わせです。

初めてページにアクセスする場合は、可能であればサーバー側でユーザーを読み込み、ページをレンダリングします。クライアント側では、ユーザーオブジェクトを使用してページを再水和し、再フェッチしないため、ちらつきはありません。

2番目のシナリオでは、クライアント側のナビゲーションを使用してページにアクセスしています。この場合、ユーザーがすでにロードされているかどうかを確認します。ユーザーオブジェクトがnullの場合、それをフェッチしようとします。

すばらしい、ユニバーサルユーザーパターンが用意されています。しかし、私たちが直面する可能性のある別の問題があります。ユーザーが2番目のタブまたはウィンドウを開いて、ログアウトボタンをクリックするとどうなりますか?

デモのユニバーサルユーザーページを2つのタブまたはウィンドウで開き、自分で試してみてください。一方のタブでログアウトをクリックしてからもう一方のタブに戻ると、ユーザーオブジェクトがまだそこにあることがわかります。

「ウィンドウフォーカスでユーザーを再フェッチする」パターンは、この問題の解決策です。

4.ウィンドウフォーカスでユーザーを再フェッチします

幸い、このメソッドを使用してイベントwindow.addEventListenerをリッスンできます。focusこのようにして、ユーザーがタブまたはウィンドウをアクティブ化するたびに通知を受け取ります。

ウィンドウイベントを処理するために、ページにフックを追加しましょう。

const windowHooks = (setIsWindowFocused: Dispatch<SetStateAction<"pristine" | "focused" | "blurred">>) => {
    useEffect(() => {
        const onFocus = () => {
            setIsWindowFocused("focused");
        };
        const onBlur = () => {
            setIsWindowFocused("blurred");
        };
        window.addEventListener('focus', onFocus);
        window.addEventListener('blur', onBlur);
        return () => {
            window.removeEventListener('focus', onFocus);
            window.removeEventListener('blur', onBlur);
        };
    }, []);
}

「isWindowFocused」アクションには、元の状態、フォーカスされた状態、ぼやけた状態の3つの状態が導入されていることに気付くでしょう。なぜ3つの州?焦点が合っている状態とぼやけている状態の2つの状態しかない場合を想像してみてください。この場合、ウィンドウがすでにフォーカスされている場合でも、常に「フォーカス」イベントを発生させる必要があります。3番目の状態(元の状態)を導入することで、これを回避できます。

もう1つの重要な観察は、コンポーネントがアンマウントされるときにイベントリスナーを削除することです。これは、メモリリークを回避するために非常に重要です。

わかりました。ウィンドウフォーカスのグローバル状態を導入しました。この状態を利用して、別のフックを追加することにより、ウィンドウフォーカスでユーザーを再フェッチしてみましょう。

useEffect(() => {
    if (disableFetchUserClientSide) {
        return;
    }
    if (disableFetchUserOnWindowFocus) {
        return;
    }
    if (isWindowFocused !== "focused") {
        return
    }
    const abort = new AbortController();
    (async () => {
        try {
            const nextUser = await ctx.client.fetchUser(abort.signal);
            if (JSON.stringify(nextUser) === JSON.stringify(user)) {
                return;
            }
            setUser(nextUser);
        } catch (e) {
        }
    })();
    return () => {
        abort.abort();
    };
}, [isWindowFocused, disableFetchUserClientSide, disableFetchUserOnWindowFocus]);

依存関係リストに状態を追加することによりisWindowFocused、ウィンドウのフォーカスが変更されるたびにこの効果がトリガーされます。イベント「pristine」と「blurred」を却下し、ウィンドウがフォーカスされている場合にのみユーザーフェッチをトリガーします。

さらに、ユーザーが実際に変更された場合にのみ、ユーザーのsetStateをトリガーするようにします。そうしないと、不要な再レンダリングまたは再フェッチがトリガーされる可能性があります。

素晴らしい!これで、アプリケーションはさまざまなシナリオで認証を処理できるようになりました。これは、実際のデータフェッチフックに進むための優れた基盤です。

5.クライアント側のクエリ

最初に確認するデータフェッチフックは、クライアント側のクエリです。ブラウザでデモページ(http:// localhost:3000 / patterns / client-side-query)を開いて、その感触をつかむことができます。

const data = useQuery.CountryWeather({
    input: {
        code: "DE",
    },
});

それで、背後にあるものは何useQuery.CountryWeatherですか?みてみましょう!

function useQueryContextWrapper<Input, Data, Role>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, query: QueryProps, args?: InternalQueryArgsWithInput<Input>): {
    result: QueryResult<Data>;
} {
    const {client} = useContext(wunderGraphContext);
    const cacheKey = client.cacheKey(query, args);
    const [statefulArgs, setStatefulArgs] = useState<InternalQueryArgsWithInput<Input> | undefined>(args);
    const [queryResult, setQueryResult] = useState<QueryResult<Data> | undefined>({status: "none"});
    useEffect(() => {
        if (lastCacheKey === "") {
            setLastCacheKey(cacheKey);
            return;
        }
        if (lastCacheKey === cacheKey) {
            return;
        }
        setLastCacheKey(cacheKey);
        setStatefulArgs(args);
        setInvalidate(invalidate + 1);
    }, [cacheKey]);
    useEffect(() => {
       const abort = new AbortController();
        setQueryResult({status: "loading"});
        (async () => {
            const result = await client.query(query, {
                ...statefulArgs,
                abortSignal: abort.signal,
            });
            setQueryResult(result as QueryResult<Data>);
        })();
        return () => {
            abort.abort();
            setQueryResult({status: "cancelled"});
        }
    }, [invalidate]);
    return {
        result: queryResult as QueryResult<Data>,
    }
}

ここで何が起こっているのか説明しましょう。まず、React.Contextを介して注入されているクライアントを取得します。次に、クエリと引数のキャッシュキーを計算します。このcacheKeyは、データを再フェッチする必要があるかどうかを判断するのに役立ちます。

操作の初期状態はに設定され{status: "none"}ます。最初のフェッチがトリガーされると、ステータスはに設定され"loading"ます。フェッチが終了すると、ステータスは"success"またはに設定され"error"ます。このフックをラップしているコンポーネントがマウント解除されている場合、ステータスはに設定され"cancelled"ます。

それ以外は、ここでは何も派手なことは起こっていません。フェッチは、useEffectがトリガーされたときにのみ発生します。これは、サーバーでフェッチを実行できないことを意味します。React.Hooksはサーバー上で実行されません。

デモを見ると、再びちらつきがあることに気付くでしょう。これは、コンポーネントをサーバーでレンダリングしていないためです。これを改善しましょう!

6.サーバー側のクエリ

クライアントだけでなくサーバーでもクエリを実行するには、フックにいくつかの変更を適用する必要があります。

useQueryまず、フックを更新しましょう。

function useQueryContextWrapper<Input, Data, Role>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, query: QueryProps, args?: InternalQueryArgsWithInput<Input>): {
    result: QueryResult<Data>;
} {
    const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
    const isServer = typeof window === 'undefined';
    const ssrEnabled = args?.disableSSR !== true && args?.lazy !== true;
    const cacheKey = client.cacheKey(query, args);
    if (isServer) {
        if (ssrEnabled) {
            if (ssrCache[cacheKey]) {
                return {
                    result: ssrCache[cacheKey] as QueryResult<Data>,
                }
            }
            const promise = client.query(query, args);
            ssrCache[cacheKey] = promise;
            throw promise;
        } else {
            ssrCache[cacheKey] = {
                status: "none",
            };
            return {
                result: ssrCache[cacheKey] as QueryResult<Data>,
            }
        }
    }
    const [invalidate, setInvalidate] = useState<number>(0);
    const [statefulArgs, setStatefulArgs] = useState<InternalQueryArgsWithInput<Input> | undefined>(args);
    const [lastCacheKey, setLastCacheKey] = useState<string>("");
    const [queryResult, setQueryResult] = useState<QueryResult<Data> | undefined>(ssrCache[cacheKey] as QueryResult<Data> || {status: "none"});
    useEffect(() => {
        if (lastCacheKey === "") {
            setLastCacheKey(cacheKey);
            return;
        }
        if (lastCacheKey === cacheKey) {
            return;
        }
        setLastCacheKey(cacheKey);
        setStatefulArgs(args);
        if (args?.debounceMillis !== undefined) {
            setDebounce(prev => prev + 1);
            return;
        }
        setInvalidate(invalidate + 1);
    }, [cacheKey]);
    useEffect(() => {
        setQueryResult({status: "loading"});
        (async () => {
            const result = await client.query(query, {
                ...statefulArgs,
                abortSignal: abort.signal,
            });
            setQueryResult(result as QueryResult<Data>);
        })();
        return () => {
            abort.abort();
            setQueryResult({status: "cancelled"});
        }
    }, [invalidate]);
    return {
        result: queryResult as QueryResult<Data>,
    }
}

useQueryフックを更新して、サーバー上にいるかどうかを確認しました。サーバー上にいる場合は、生成されたキャッシュキーのデータがすでに解決されているかどうかを確認します。データが解決された場合は、それを返します。それ以外の場合は、クライアントを使用してPromiseを使用してクエリを実行します。しかし、問題があります。サーバーでのレンダリング中に非同期コードを実行することは許可されていません。したがって、理論的には、約束が解決するのを「待つ」ことはできません。

代わりに、トリックを使用する必要があります。レンダリングを「一時停止」する必要があります。これは、作成したばかりの約束を「投げる」ことで実現できます。

サーバー上で囲んでいるコンポーネントをレンダリングしていると想像してください。私たちにできることは、各コンポーネントのレンダリングプロセスをtry/catchブロックにラップすることです。そのようなコンポーネントの1つがプロミスをスローした場合、それをキャッチし、プロミスが解決するまで待ってから、コンポーネントを再レンダリングできます。

約束が解決されると、キャッシュキーに結果を入力できるようになります。このようにして、コンポーネントを2回目にレンダリングしようとすると、すぐにデータを返すことができます。このメソッドを使用すると、コンポーネントツリーを移動して、サーバー側のレンダリングが有効になっているすべてのクエリを実行できます。

このtry/catchメソッドを実装する方法を疑問に思うかもしれません。幸い、最初から始める必要はありません。これを行うために使用できるreact-ssr-prepassというライブラリがあります。

getInitialPropsこれを関数に適用してみましょう。

WithWunderGraph.getInitialProps = async (ctx: NextPageContext) => {


    const pageProps = (Page as NextPage).getInitialProps ? await (Page as NextPage).getInitialProps!(ctx as any) : {};
    const ssrCache: { [key: string]: any } = {};


    if (typeof window !== 'undefined') {
        // we're on the client
        // no need to do all the SSR stuff
        return {...pageProps, ssrCache};
    }


    const cookieHeader = ctx.req?.headers.cookie;
    if (typeof cookieHeader === "string") {
        defaultContextProperties.client.setExtraHeaders({
            Cookie: cookieHeader,
        });
    }


    let ssrUser: User<Role> | null = null;


    if (options?.disableFetchUserServerSide !== true) {
        try {
            ssrUser = await defaultContextProperties.client.fetchUser();
        } catch (e) {
        }
    }


    const AppTree = ctx.AppTree;


    const App = createElement(wunderGraphContext.Provider, {
        value: {
            ...defaultContextProperties,
            user: ssrUser,
        },
    }, createElement(AppTree, {
        pageProps: {
            ...pageProps,
        },
        ssrCache,
        user: ssrUser
    }));


    await ssrPrepass(App);
    const keys = Object.keys(ssrCache).filter(key => typeof ssrCache[key].then === 'function').map(key => ({
        key,
        value: ssrCache[key]
    })) as { key: string, value: Promise<any> }[];
    if (keys.length !== 0) {
        const promises = keys.map(key => key.value);
        const results = await Promise.all(promises);
        for (let i = 0; i < keys.length; i++) {
            const key = keys[i].key;
            ssrCache[key] = results[i];
        }
    }


    return {...pageProps, ssrCache, user: ssrUser};
};

オブジェクトには、オブジェクトctxだけでなくオブジェクトも含まれます。オブジェクトを使用して、コンポーネントツリー全体を構築し、コンテキストプロバイダー、オブジェクト、およびオブジェクトを挿入できます。reqAppTreeAppTreessrCacheuser

次に、この関数を使用しssrPrepassてコンポーネントツリーをトラバースし、サーバー側でのレンダリングが有効になっているすべてのクエリを実行できます。その後、すべてのPromiseから結果を抽出し、ssrCacheオブジェクトにデータを入力します。最後に、pagePropsオブジェクトとオブジェクト、およびオブジェクトssrCacheを返しuserます。

素晴らしい!これで、サーバー側のレンダリングをuseQueryフックに適用できるようになりました。

サーバー側のレンダリングをコンポーネントに実装getServerSidePropsする必要がないように完全に分離したことは言及する価値があります。Pageこれには、議論することが重要ないくつかの影響があります。

まず、でデータの依存関係を宣言する必要があるという問題を解決しましたgetServerSideProps。useQueryフックはコンポーネントツリーのどこにでも自由に配置でき、常に実行されます。

一方、このアプローチには、このページが静的に最適化されないという欠点があります。代わりに、ページは常にサーバーでレンダリングされます。つまり、ページを提供するにはサーバーが実行されている必要があります。もう1つのアプローチは、静的にレンダリングされたページを作成することです。これは、CDNから完全に提供できます。

とはいえ、このガイドでは、ユーザーに応じて変化する動的コンテンツを提供することが目標であると想定しています。このシナリオでは、データをフェッチするときにユーザーコンテキストがないため、ページを静的にレンダリングすることはできません。

これまでに達成したことは素晴らしいことです。しかし、ユーザーがしばらくウィンドウを離れて戻ってきた場合はどうなるでしょうか。過去に取得したデータが古くなっている可能性はありますか?もしそうなら、私たちはこの状況にどのように対処できますか?次のパターンへ!

7.ウィンドウフォーカスでクエリを再フェッチします

幸いなことに、3つの異なるウィンドウフォーカス状態(元の状態、ぼやけた状態、フォーカスされた状態)を伝播するために、グローバルコンテキストオブジェクトをすでに実装しています。

「フォーカスされた」状態を利用して、クエリの再フェッチをトリガーしてみましょう。

「invalidate」カウンターを使用してクエリの再フェッチをトリガーしていたことを思い出してください。新しい効果を追加して、ウィンドウがフォーカスされているときはいつでもこのカウンターを増やすことができます。

useEffect(() => {
    if (!refetchOnWindowFocus) {
        return;
    }
    if (isWindowFocused !== "focused") {
        return;
    }
    setInvalidate(prev => prev + 1);
}, [refetchOnWindowFocus, isWindowFocused]);

それでおしまい!refetchOnWindowFocusがfalseに設定されている場合、またはウィンドウがフォーカスされていない場合は、すべてのイベントを却下します。それ以外の場合は、無効化カウンターを増やし、クエリの再フェッチをトリガーします。

デモをフォローしている場合は、refetch-query-on-window-focusページをご覧ください。

構成を含むフックは、次のようになります。

const data = useQuery.CountryWeather({
    input: {
        code: "DE",
    },
    disableSSR: true,
    refetchOnWindowFocus: true,
});

あっという間でした!次のパターンである遅延読み込みに移りましょう。

8.レイジークエリ

問題の説明で説明したように、一部の操作は特定のイベントの後でのみ実行する必要があります。それまでは、実行を延期する必要があります。

怠惰なクエリのページを見てみましょう。

const [args,setArgs] = useState<QueryArgsWithInput<CountryWeatherInput>>({
    input: {
        code: "DE",
    },
    lazy: true,
});

lazyをtrueに設定すると、フックが「lazy」に設定されます。それでは、実装を見てみましょう。

useEffect(() => {
    if (lazy && invalidate === 0) {
        setQueryResult({
            status: "lazy",
        });
        return;
    }
    const abort = new AbortController();
    setQueryResult({status: "loading"});
    (async () => {
        const result = await client.query(query, {
            ...statefulArgs,
            abortSignal: abort.signal,
        });
        setQueryResult(result as QueryResult<Data>);
    })();
    return () => {
        abort.abort();
        setQueryResult({status: "cancelled"});
    }
}, [invalidate]);
const refetch = useCallback((args?: InternalQueryArgsWithInput<Input>) => {
    if (args !== undefined) {
        setStatefulArgs(args);
    }
    setInvalidate(prev => prev + 1);
}, []);

このフックを初めて実行すると、lazyがtrueに設定され、invalidateが0に設定されます。これは、エフェクトフックが早期に戻り、クエリ結果が「lazy」に設定されることを意味します。このシナリオでは、フェッチは実行されません。

クエリを実行する場合は、invalidateを1増やす必要があります。これはrefetch、useQueryフックを呼び出すことで実行できます。

それでおしまい!遅延読み込みが実装されました。

次の問題に移りましょう。クエリを頻繁にフェッチしないようにユーザー入力をデバウンスします。

9.デバウンスクエリ

ユーザーが特定の都市の天気を取得したいとします。私の故郷はドイツの真ん中にある「フランクフルト・アム・マイン」です。その検索語の長さは17文字です。ユーザーが入力している間、どのくらいの頻度でクエリを取得する必要がありますか?17回?一度?多分二度?

答えは真ん中のどこかになりますが、それは間違いなく17回ではありません。では、この動作をどのように実装できますか?useQueryフックの実装を見てみましょう。

useEffect(() => {
    if (debounce === 0) {
        return;
    }
    const cancel = setTimeout(() => {
        setInvalidate(prev => prev + 1);
    }, args?.debounceMillis || 0);
    return () => clearTimeout(cancel);
}, [debounce]);
useEffect(() => {
    if (lastCacheKey === "") {
        setLastCacheKey(cacheKey);
        return;
    }
    if (lastCacheKey === cacheKey) {
        return;
    }
    setLastCacheKey(cacheKey);
    setStatefulArgs(args);
    if (args?.debounceMillis !== undefined) {
        setDebounce(prev => prev + 1);
        return;
    }
    setInvalidate(invalidate + 1);
}, [cacheKey]);

まず、2番目のuseEffectを見てみましょう。これは、依存関係としてcacheKeyを持つものです。無効化カウンターを増やす前に、操作の引数にdebounceMillisプロパティが含まれているかどうかを確認していることがわかります。その場合、無効化カウンターをすぐに増やすことはありません。代わりに、デバウンスカウンターを増やします。

デバウンスカウンターは依存関係であるため、デバウンスカウンターを増やすと、最初のuseEffectがトリガーされます。デバウンスカウンタが初期値である0の場合、何もすることがないため、すぐに戻ります。それ以外の場合は、setTimeoutを使用してタイマーを開始します。タイムアウトがトリガーされると、無効化カウンターが増加します。

setTimeoutを使用したエフェクトの特別な点は、エフェクトフックのreturn関数を利用してタイムアウトをクリアしていることです。これは、ユーザーがデバウンス時間よりも速く入力した場合、タイマーは常にクリアされ、無効化カウンターは増加しないことを意味します。完全なデバウンス時間が経過した場合にのみ、無効化カウンターが増加します。

開発者がsetTimeoutを使用しているのに、返されるオブジェクトを処理するのを忘れていることがよくあります。setTimeoutの戻り値を処理しないと、メモリリークが発生する可能性があります。これは、タイムアウトがトリガーされる前に、囲んでいるReactコンポーネントがアンマウントされる可能性もあるためです。

遊んでみたい場合は、デモにアクセスして、さまざまなデバウンス時間を使用してさまざまな検索用語を入力してみてください。

素晴らしい!ユーザー入力をデバウンスするための優れたソリューションがあります。次に、ユーザーの認証が必要な操作を見てみましょう。サーバー側で保護されたクエリから始めます。

10.サーバー側で保護されたクエリ

ユーザーの認証が必要なダッシュボードをレンダリングしているとしましょう。ダッシュボードには、ユーザー固有のデータも表示されます。これをどのように実装できますか?ここでも、useQueryフックを変更する必要があります。

const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
const isServer = typeof window === 'undefined';
const ssrEnabled = args?.disableSSR !== true && args?.lazy !== true;
const cacheKey = client.cacheKey(query, args);
if (isServer) {
    if (query.requiresAuthentication && user === null) {
        ssrCache[cacheKey] = {
            status: "requires_authentication"
        };
        return {
            result: ssrCache[cacheKey] as QueryResult<Data>,
            refetch: () => {
            },
        };
    }
    if (ssrEnabled) {
        if (ssrCache[cacheKey]) {
            return {
                result: ssrCache[cacheKey] as QueryResult<Data>,
                refetch: () => Promise.resolve(ssrCache[cacheKey] as QueryResult<Data>),
            }
        }
        const promise = client.query(query, args);
        ssrCache[cacheKey] = promise;
        throw promise;
    } else {
        ssrCache[cacheKey] = {
            status: "none",
        };
        return {
            result: ssrCache[cacheKey] as QueryResult<Data>,
            refetch: () => ({}),
        }
    }
}

パターン2のサーバー側ユーザーで説明したように、ユーザーオブジェクトをフェッチgetInitialPropsしてコンテキストに挿入するためのロジックを既に実装しています。また、コンテキストにも挿入されるクライアントにユーザーCookieを挿入しました。一緒に、サーバー側の保護されたクエリを実装する準備が整いました。

サーバー上にいる場合は、クエリに認証が必要かどうかを確認します。これは、クエリメタデータで定義されている静的な情報です。ユーザーオブジェクトがnullの場合、つまりユーザーが認証されていない場合、ステータスが「requires_authentication」の結果が返されます。それ以外の場合は、先に進んでpromiseをスローするか、キャッシュから結果を返します。

デモでサーバー側の保護されたクエリに移動すると、この実装を試して、ログインおよびログアウトしたときの動作を確認できます。

それだけです、魔法はありません。それほど複雑ではありませんでしたね。ええと、サーバーはフックを禁止しているので、ロジックがずっと簡単になります。次に、クライアントに同じロジックを実装するために必要なものを見てみましょう。

11.クライアント側で保護されたクエリ

クライアントに同じロジックを実装するには、useQueryフックをもう一度変更する必要があります。

useEffect(() => {
    if (query.requiresAuthentication && user === null) {
        setQueryResult({
            status: "requires_authentication",
        });
        return;
    }
    if (lazy && invalidate === 0) {
        setQueryResult({
            status: "lazy",
        });
        return;
    }
    const abort = new AbortController();
    if (queryResult?.status === "ok") {
        setQueryResult({...queryResult, refetching: true});
    } else {
        setQueryResult({status: "loading"});
    }
    (async () => {
        const result = await client.query(query, {
            ...statefulArgs,
            abortSignal: abort.signal,
        });
        setQueryResult(result as QueryResult<Data>);
    })();
    return () => {
        abort.abort();
        setQueryResult({status: "cancelled"});
    }
}, [invalidate, user]);

ご覧のとおり、エフェクトの依存関係にユーザーオブジェクトを追加しました。クエリに認証が必要であるが、ユーザーオブジェクトがnullの場合、クエリ結果を「requires_authentication」に設定し、早期に戻ります。フェッチは発生しません。このチェックに合格すると、クエリは通常どおり実行されます。

ユーザーオブジェクトをフェッチ効果の依存関係にすることには、2つの優れた副作用もあります。

たとえば、クエリではユーザーが認証される必要がありますが、現在は認証されていません。最初のクエリ結果は「requires_authentication」です。ユーザーがログインすると、ユーザーオブジェクトはコンテキストオブジェクトを介して更新されます。ユーザーオブジェクトはフェッチ効果の依存関係であるため、すべてのクエリが再度実行され、クエリ結果が更新されます。

一方、クエリでユーザーの認証が必要で、ユーザーがログアウトしたばかりの場合は、すべてのクエリが自動的に無効になり、結果が「requires_authentication」に設定されます。

素晴らしい!これで、クライアント側で保護されたクエリパターンが実装されました。しかし、それはまだ理想的な結果ではありません。

サーバー側で保護されたクエリを使用している場合、クライアント側のナビゲーションは適切に処理されません。一方、クライアント側で保護されたクエリのみを使用している場合は、常に厄介なちらつきが再び発生します。

これらの問題を解決するには、これらのパターンの両方を組み合わせる必要があります。これにより、ユニバーサル保護されたクエリパターンになります。

12.ユニバーサルプロテクトクエリ

すでにすべてのロジックを実装しているため、このパターンに追加の変更は必要ありません。ユニバーサル保護されたクエリパターンをアクティブ化するようにページを構成するだけです。

ユニバーサル保護されたクエリページのコードは次のとおりです。

const UniversalProtectedQuery = () => {
    const {user,login,logout} = useWunderGraph();
    const data = useQuery.ProtectedWeather({
        input: {
            city: "Berlin",
        },
    });
    return (
        <div>
            <h1>Universal Protected Query</h1>
            <p>{JSON.stringify(user)}</p>
            <p>{JSON.stringify(data)}</p>
            <button onClick={() => login(AuthProviders.github)}>Login</button>
            <button onClick={() => logout()}>Logout</button>
        </div>
    )
}


export default withWunderGraph(UniversalProtectedQuery);

デモを試して、ログインおよびログアウトしたときの動作を確認してください。また、ページを更新するか、クライアント側のナビゲーションを使用してみてください。

このパターンのすごいところは、ページの実際の実装がいかに簡単かということです。「ProtectedWeather」クエリフックは、クライアント側とサーバー側の両方で、認証処理の複雑さをすべて抽象化します。

13.保護されていない突然変異

そうですね、これまでクエリに多くの時間を費やしてきましたが、ミューテーションについてはどうでしょうか。認証を必要としない、保護されていないミューテーションから始めましょう。ミューテーションフックは、クエリフックよりも実装がはるかに簡単であることがわかります。

function useMutationContextWrapper<Role, Input = never, Data = never>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, mutation: MutationProps): {
    result: MutationResult<Data>;
    mutate: (args?: InternalMutationArgsWithInput<Input>) => Promise<MutationResult<Data>>;
} {
    const {client, user} = useContext(wunderGraphContext);
    const [result, setResult] = useState<MutationResult<Data>>(mutation.requiresAuthentication && user === null ? {status: "requires_authentication"} : {status: "none"});
    const mutate = useCallback(async (args?: InternalMutationArgsWithInput<Input>): Promise<MutationResult<Data>> => {
        setResult({status: "loading"});
        const result = await client.mutate(mutation, args);
        setResult(result as any);
        return result as any;
    }, []);
    return {
        result,
        mutate
    }
}

ミューテーションは自動的にトリガーされません。これは、useEffectを使用してミューテーションをトリガーしていないことを意味します。代わりに、useCallbackフックを利用して、呼び出すことができる「変異」関数を作成しています。

呼び出されたら、結果の状態を「読み込み中」に設定してから、ミューテーションを呼び出します。ミューテーションが終了したら、結果の状態をミューテーション結果に設定します。これは成功または失敗の可能性があります。最後に、結果とmutate関数の両方を返します。

このパターンで遊んでみたい場合は、保護されていないミューテーションページをご覧ください。

これはかなり簡単でした。認証を追加して、複雑さを加えましょう。

14.保護された突然変異

function useMutationContextWrapper<Role, Input = never, Data = never>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, mutation: MutationProps): {
    result: MutationResult<Data>;
    mutate: (args?: InternalMutationArgsWithInput<Input>) => Promise<MutationResult<Data>>;
} {
    const {client, user} = useContext(wunderGraphContext);
    const [result, setResult] = useState<MutationResult<Data>>(mutation.requiresAuthentication && user === null ? {status: "requires_authentication"} : {status: "none"});
    const mutate = useCallback(async (args?: InternalMutationArgsWithInput<Input>): Promise<MutationResult<Data>> => {
        if (mutation.requiresAuthentication && user === null) {
            return {status: "requires_authentication"}
        }
        setResult({status: "loading"});
        const result = await client.mutate(mutation, args);
        setResult(result as any);
        return result as any;
    }, [user]);
    useEffect(() => {
        if (!mutation.requiresAuthentication) {
            return
        }
        if (user === null) {
            if (result.status !== "requires_authentication") {
                setResult({status: "requires_authentication"});
            }
            return;
        }
        if (result.status !== "none") {
            setResult({status: "none"});
        }
    }, [user]);
    return {
        result,
        mutate
    }
}

保護されたクエリパターンと同様に、コンテキストからコールバックにユーザーオブジェクトを挿入します。ミューテーションで認証が必要な場合は、ユーザーがnullかどうかを確認します。ユーザーがnullの場合、結果を「requires_authentication」に設定し、早期に戻ります。

さらに、ユーザーがnullかどうかを確認するエフェクトを追加します。ユーザーがnullの場合、結果を「requires_authentication」に設定します。これは、ユーザーが認証されているかどうかに応じて、ミューテーションが自動的に「requires_authentication」または「none」状態になるようにするためです。それ以外の場合は、最初にミューテーションを呼び出して、ミューテーションを呼び出すことができないことを理解する必要があります。ミューテーションが可能かどうかが前もって明確になっていると、開発者のエクスペリエンスが向上すると思います。

了解しました。保護されたミューテーションが実装されました。保護されているかどうかにかかわらず、サーバー側のミューテーションに関するセクションがないのはなぜか疑問に思われるかもしれません。これは、ミューテーションは常にユーザーの操作によってトリガーされるためです。したがって、サーバーに何かを実装する必要はありません。

とはいえ、突然変異、副作用には1つの問題が残っています!タスクのリストとタスクを変更するミューテーションの間に依存関係がある場合はどうなりますか?それを実現させましょう!

15.ミューテーションの成功時にマウントされた操作を再フェッチする

これを機能させるには、ミューテーションコールバックとクエリフックの両方を変更する必要があります。ミューテーションコールバックから始めましょう。

const {client, setRefetchMountedOperations, user} = useContext(wunderGraphContext);
const mutate = useCallback(async (args?: InternalMutationArgsWithInput<Input>): Promise<MutationResult<Data>> => {
    if (mutation.requiresAuthentication && user === null) {
        return {status: "requires_authentication"}
    }
    setResult({status: "loading"});
    const result = await client.mutate(mutation, args);
    setResult(result as any);
    if (result.status === "ok" && args?.refetchMountedOperationsOnSuccess === true) {
        setRefetchMountedOperations(prev => prev + 1);
    }
    return result as any;
}, [user]);

私たちの目標は、ミューテーションが成功したときに、現在マウントされているすべてのクエリを無効にすることです。これを行うには、Reactコンテキストを介して保存および伝播されるさらに別のグローバル状態オブジェクトを導入します。この状態オブジェクトを「refetchMountedOperationsOnSuccess」と呼びます。これは単純なカウンターです。ミューテーションコールバックが成功した場合は、カウンターをインクリメントします。これは、現在マウントされているすべてのクエリを無効にするのに十分なはずです。

2番目のステップは、クエリフックを変更することです。

const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
useEffect(() => {
    if (queryResult?.status === "lazy" || queryResult?.status === "none") {
        return;
    }
    setInvalidate(prev => prev + 1);
}, [refetchMountedOperations]);

すでに「無効化」カウンターに精通している必要があります。現在、コンテキストから挿入された「refetchMountedOperations」カウンターの増分を処理するための別のエフェクトを追加しています。ステータスが「レイジー」または「なし」の場合、なぜ早く戻るのかと疑問に思われるかもしれません。

「レイジー」の場合、このクエリはまだ実行されていないことがわかります。開発者は、手動でトリガーされた場合にのみクエリを実行することを意図しています。したがって、遅延クエリをスキップして、手動でトリガーされるまで待機します。

「なし」の場合も同じルールが適用されます。これは、たとえば、クエリがサーバー側でのみレンダリングされているが、クライアント側のナビゲーションを介して現在のページに移動した場合に発生する可能性があります。このような場合、クエリはまだ実行されていないため、「無効化」できるものはありません。また、ミューテーションの副作用によってまだ実行されていないクエリを誤ってトリガーしたくありません。

これを実際に体験してみませんか?[ミューテーションの成功でマウントされた操作を再フェッチする]ページに移動します。

涼しい!クエリとミューテーションは完了です。次に、サブスクリプションのフックの実装について見ていきます。

16.クライアント側のサブスクリプション

サブスクリプションを実装するには、新しい専用フックを作成する必要があります。

function useSubscriptionContextWrapper<Input, Data, Role>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, subscription: SubscriptionProps, args?: InternalSubscriptionArgsWithInput<Input>): {
    result: SubscriptionResult<Data>;
} {
    const {ssrCache, client} = useContext(wunderGraphContext);
    const cacheKey = client.cacheKey(subscription, args);
    const [invalidate, setInvalidate] = useState<number>(0);
    const [subscriptionResult, setSubscriptionResult] = useState<SubscriptionResult<Data> | undefined>(ssrCache[cacheKey] as SubscriptionResult<Data> || {status: "none"});
    useEffect(() => {
        if (subscriptionResult?.status === "ok") {
            setSubscriptionResult({...subscriptionResult, streamState: "restarting"});
        } else {
            setSubscriptionResult({status: "loading"});
        }
        const abort = new AbortController();
        client.subscribe(subscription, (response: SubscriptionResult<Data>) => {
            setSubscriptionResult(response as any);
        }, {
            ...args,
            abortSignal: abort.signal
        });
        return () => {
            abort.abort();
        }
    }, [invalidate]);
    return {
        result: subscriptionResult as SubscriptionResult<Data>
    }
}

このフックの実装は、クエリフックに似ています。囲んでいるコンポーネントがマウントされると自動的にトリガーされるため、「useEffect」フックを再度使用しています。

コンポーネントがアンマウントされたときにサブスクリプションが確実に中止されるように、中止シグナルをクライアントに渡すことが重要です。さらに、クエリフックと同様に、無効化カウンターがインクリメントされたときに、サブスクリプションをキャンセルして再開します。

この時点では簡潔にするために認証を省略していますが、これはクエリフックと非常によく似ていると考えられます。

例で遊んでみませんか?クライアント側のサブスクリプションページに移動します。

ただし、注意すべき点の1つは、サブスクリプションの動作がクエリとは異なることです。サブスクリプションは、継続的に更新されるデータのストリームです。これは、サブスクリプションを開いたままにしておく期間について考える必要があることを意味します。それは永遠に開いたままにする必要がありますか?または、サブスクリプションを停止して再開したい場合がありますか?

そのようなケースの1つは、ユーザーがウィンドウをぼかした場合です。これは、ユーザーがアプリケーションをアクティブに使用しなくなったことを意味します。

17.ウィンドウブラーでサブスクリプションを停止します

ユーザーがウィンドウをぼかしたときにサブスクリプションを停止するには、サブスクリプションフックを拡張する必要があります。

function useSubscriptionContextWrapper<Input, Data, Role>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, subscription: SubscriptionProps, args?: InternalSubscriptionArgsWithInput<Input>): {
    result: SubscriptionResult<Data>;
} {
    const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
    const isServer = typeof window === 'undefined';
    const ssrEnabled = args?.disableSSR !== true;
    const cacheKey = client.cacheKey(subscription, args);
    const [stop, setStop] = useState(false);
    const [invalidate, setInvalidate] = useState<number>(0);
    const [stopOnWindowBlur] = useState(args?.stopOnWindowBlur === true);
    const [subscriptionResult, setSubscriptionResult] = useState<SubscriptionResult<Data> | undefined>(ssrCache[cacheKey] as SubscriptionResult<Data> || {status: "none"});
    useEffect(() => {
        if (stop) {
            if (subscriptionResult?.status === "ok") {
                setSubscriptionResult({...subscriptionResult, streamState: "stopped"});
            } else {
                setSubscriptionResult({status: "none"});
            }
            return;
        }
        if (subscriptionResult?.status === "ok") {
            setSubscriptionResult({...subscriptionResult, streamState: "restarting"});
        } else {
            setSubscriptionResult({status: "loading"});
        }
        const abort = new AbortController();
        client.subscribe(subscription, (response: SubscriptionResult<Data>) => {
            setSubscriptionResult(response as any);
        }, {
            ...args,
            abortSignal: abort.signal
        });
        return () => {
            abort.abort();
        }
    }, [stop, refetchMountedOperations, invalidate, user]);
    useEffect(() => {
        if (!stopOnWindowBlur) {
            return
        }
        if (isWindowFocused === "focused") {
            setStop(false);
        }
        if (isWindowFocused === "blurred") {
            setStop(true);
        }
    }, [stopOnWindowBlur, isWindowFocused]);
    return {
        result: subscriptionResult as SubscriptionResult<Data>
    }
}

これを機能させるために、「stop」と呼ばれる新しいステートフル変数を導入します。デフォルトの状態はfalseになりますが、ユーザーがウィンドウをぼかすと、状態がtrueに設定されます。彼らがウィンドウに再び入る(フォーカス)場合、状態をfalseに戻します。開発者が「stopOnWindowBlur」をfalseに設定した場合、これは無視されます。これは、サブスクリプションの「args」オブジェクトで構成できます。

さらに、サブスクリプションの依存関係に停止変数を追加する必要があります。それでおしまい!ウィンドウイベントをグローバルに処理したことは非常に便利です。これにより、他のすべてのフックの実装がはるかに簡単になります。

実装を体験する最良の方法は、クライアント側のサブスクリプションページを開き、Chrome DevToolsコンソール(または別のブラウザを使用している場合は同様)のネットワークタブを注意深く監視することです。

最初に説明した問題の1つに戻ると、サブスクリプションのサーバー側レンダリングを実装して、サブスクリプションを「ユニバーサル」にする方法についての質問に答える必要があります。

18.ユニバーサルサブスクリプション

サブスクリプションではサーバー側のレンダリングは不可能だと思われるかもしれません。つまり、データのストリームをどのようにサーバーレンダリングする必要がありますか?

このブログを定期的に読んでいる場合は、サブスクリプションの実装をご存知かもしれません。別のブログで説明したように、EventSource(SSE)およびFetchAPIと互換性のある方法でGraphQLサブスクリプションを実装しました。

また、実装に1つの特別なフラグを追加しました。クライアントは、クエリパラメータ「wg_subscribe_once」をtrueに設定できます。これが意味するのは、このフラグが設定されたサブスクリプションは本質的にクエリであるということです。

クエリをフェッチするためのクライアントの実装は次のとおりです。

const params = this.queryString({
    wg_variables: args?.input,
    wg_api_hash: this.applicationHash,
    wg_subscribe_once: args?.subscribeOnce,
});
const headers: Headers = {
    ...this.extraHeaders,
    Accept: "application/json",
    "WG-SDK-Version": this.sdkVersion,
};
const defaultOrCustomFetch = this.customFetch || globalThis.fetch;
const url = this.baseURL + "/" + this.applicationPath + "/operations/" + query.operationName + params;
const response = await defaultOrCustomFetch(url,
    {
        headers,
        method: 'GET',
        credentials: "include",
        mode: "cors",
    }
);

変数、構成のハッシュ、およびsubscribeOnceフラグを取得し、それらをクエリ文字列にエンコードします。一度サブスクライブが設定されている場合、サブスクリプションの最初の結果のみが必要であることはサーバーにとって明らかです。

全体像を把握するために、クライアント側サブスクリプションの実装も見てみましょう。

private subscribeWithSSE = <S extends SubscriptionProps, Input, Data>(subscription: S, cb: (response: SubscriptionResult<Data>) => void, args?: InternalSubscriptionArgs) => {
    (async () => {
        try {
            const params = this.queryString({
                wg_variables: args?.input,
                wg_live: subscription.isLiveQuery ? true : undefined,
                wg_sse: true,
                wg_sdk_version: this.sdkVersion,
            });
            const url = this.baseURL + "/" + this.applicationPath + "/operations/" + subscription.operationName + params;
            const eventSource = new EventSource(url, {
                withCredentials: true,
            });
            eventSource.addEventListener('message', ev => {
                const responseJSON = JSON.parse(ev.data);
                // omitted for brevity
                if (responseJSON.data) {
                    cb({
                        status: "ok",
                        streamState: "streaming",
                        data: responseJSON.data,
                    });
                }
            });
            if (args?.abortSignal) {
                args.abortSignal.addEventListener("abort", () => eventSource.close());
            }
        } catch (e: any) {
            // omitted for brevity
        }
    })();
};

サブスクリプションクライアントの実装は、コールバックでEventSource APIを使用することを除いて、クエリクライアントに似ています。EventSourceが利用できない場合は、Fetch APIにフォールバックしますが、追加の価値があまりないため、実装をブログ投稿から除外します。

これから取り除く必要がある唯一の重要なことは、中止シグナルにリスナーを追加することです。囲んでいるコンポーネントがアンマウントまたは無効化されると、中止イベントがトリガーされ、EventSourceが閉じられます。

何らかの非同期作業を行う場合は、キャンセルを適切に処理することを常に確認する必要があることに注意してください。そうしないと、メモリリークが発生する可能性があります。

これで、サブスクリプションクライアントの実装に気づきました。クライアントとサーバーの両方で使用できる使いやすいサブスクリプションフックでクライアントをラップしましょう。

const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
const isServer = typeof window === 'undefined';
const ssrEnabled = args?.disableSSR !== true;
const cacheKey = client.cacheKey(subscription, args);
if (isServer) {
    if (ssrEnabled) {
        if (ssrCache[cacheKey]) {
            return {
                result: ssrCache[cacheKey] as SubscriptionResult<Data>
            }
        }
        const promise = client.query(subscription, {...args, subscribeOnce: true});
        ssrCache[cacheKey] = promise;
        throw promise;
    } else {
        ssrCache[cacheKey] = {
            status: "none",
        }
        return {
            result: ssrCache[cacheKey] as SubscriptionResult<Data>
        }
    }
}

useQueryフックと同様に、サーバー側レンダリング用のコードブランチを追加します。サーバー上にいて、まだデータがない場合は、subscribeOnceフラグをtrueに設定して「クエリ」リクエストを行います。上記のように、フラグsubscribeOnceがtrueに設定されているサブスクリプションは、最初の結果のみを返すため、クエリのように動作します。そのため、のclient.query()代わりにを使用しclient.subscribe()ます。

サブスクリプションの実装に関するブログ投稿へのコメントによると、サブスクリプションをステートレスにすることはそれほど重要ではありません。この時点で、なぜこのルートを選択したのかが明確になることを願っています。フェッチのサポートはNodeJSに導入されたばかりで、それ以前でも、ポリフィルとしてノードフェッチがありました。WebSocketを使用してサーバーでサブスクリプションを開始することは間違いなく可能ですが、最終的には、サーバーでのWebSocket接続について心配する必要がなく、FetchAPIを使用する方がはるかに簡単だと思います。

この実装を試す最良の方法は、ユニバーサルサブスクリプションページに移動することです。ページを更新するときは、最初のリクエストの「プレビュー」を確認してください。クライアント側のサブスクリプションと比較して、ページがサーバーレンダリングされることがわかります。クライアントが再水和されると、ユーザーインターフェイスを最新の状態に保つために、クライアントが自動的にサブスクリプションを開始します。

それは大変な作業でしたが、まだ完了していません。サブスクリプションも認証を使用して保護する必要があります。サブスクリプションフックにロジックを追加しましょう。

19.保護されたサブスクリプション

通常のクエリフックと非常によく似ていることに気付くでしょう。

const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
const [subscriptionResult, setSubscriptionResult] = useState<SubscriptionResult<Data> | undefined>(ssrCache[cacheKey] as SubscriptionResult<Data> || {status: "none"});
useEffect(() => {
    if (subscription.requiresAuthentication && user === null) {
        setSubscriptionResult({
            status: "requires_authentication",
        });
        return;
    }
    if (stop) {
        if (subscriptionResult?.status === "ok") {
            setSubscriptionResult({...subscriptionResult, streamState: "stopped"});
        } else {
            setSubscriptionResult({status: "none"});
        }
        return;
    }
    if (subscriptionResult?.status === "ok") {
        setSubscriptionResult({...subscriptionResult, streamState: "restarting"});
    } else {
        setSubscriptionResult({status: "loading"});
    }
    const abort = new AbortController();
    client.subscribe(subscription, (response: SubscriptionResult<Data>) => {
        setSubscriptionResult(response as any);
    }, {
        ...args,
        abortSignal: abort.signal
    });
    return () => {
        abort.abort();
    }
}, [stop, refetchMountedOperations, invalidate, user]);

まず、エフェクトへの依存関係としてユーザーを追加する必要があります。これにより、ユーザーが変更するたびにエフェクトがトリガーされます。次に、サブスクリプションのメタデータをチェックして、認証が必要かどうかを確認する必要があります。含まれている場合は、ユーザーがログインしているかどうかを確認します。ユーザーがログインしている場合は、サブスクリプションを続行します。ユーザーがログインしていない場合は、サブスクリプションの結果を「requires_authentication」に設定します。

それでおしまい!認証対応のユニバーサルサブスクリプションが完了しました!最終結果を見てみましょう。

const ProtectedSubscription = () => {
    const {login,logout,user} = useWunderGraph();
    const data = useSubscription.ProtectedPriceUpdates();
    return (
        <div>
            <p>{JSON.stringify(user)}</p>
            <p style={{height: "8vh"}}>{JSON.stringify(data)}</p>
            <button onClick={() => login(AuthProviders.github)}>Login</button>
            <button onClick={() => logout()}>Logout</button>
        </div>
    )
}


export default withWunderGraph(ProtectedSubscription);

単純なAPIの背後にこれほど多くの複雑さを隠すことができるのは素晴らしいことではありませんか?認証、ウィンドウのフォーカスとぼかし、サーバー側のレンダリング、クライアント側のレンダリング、サーバーからクライアントへのデータの受け渡し、クライアントの適切な再水和など、これらすべてが私たちのために処理されます。

その上、クライアントは主にジェネリックを使用しており、生成されたコードの小さなレイヤーでラップされているため、クライアント全体が完全にタイプセーフになっています。覚えていれば、型安全性は私たちの要件の1つでした。

一部のAPIクライアントは、タイプセーフにすることができます。その他のコードを追加すると、タイプセーフになります。私たちのアプローチでは、汎用クライアントと自動生成されたタイプにより、クライアントは常にタイプセーフです。

これまでのところ、「純粋な」JavaScriptクライアントを追加するように依頼された人は誰もいないことは私たちにとって明白です。私たちのユーザーは、すべてが箱から出してタイプセーフであることを受け入れ、感謝しているようです。型安全性は、開発者がエラーを減らし、コードをよりよく理解するのに役立つと信じています。

保護されたユニバーサルサブスクリプションを自分で試してみませんか?デモの保護されたサブスクリプションページをチェックしてください。最高の洞察を得るために、ChromeDevToolsと[ネットワーク]タブを確認することを忘れないでください。

最後に、サブスクリプションが完了しました。あと2つのパターンがあり、これで完全に完了です。

20.クライアント側のライブクエリ

最後に取り上げるパターンはライブクエリです。ライブクエリは、クライアント側での動作がサブスクリプションに似ています。それらが異なるのはサーバー側です。

まず、ライブクエリがサーバー上でどのように機能するのか、そしてなぜそれらが役立つのかについて説明しましょう。クライアントがライブクエリを「サブスクライブ」すると、サーバーは変更についてオリジンサーバーのポーリングを開始します。これは、構成可能な間隔で、たとえば1秒ごとに行われます。サーバーは変更を受信すると、データをハッシュし、最後の変更のハッシュと比較します。ハッシュが異なる場合、サーバーは新しいデータをクライアントに送信します。ハッシュが同じである場合、何も変更されていないことがわかっているため、クライアントには何も送信しません。

ライブクエリが役立つのはなぜですか。まず、既存のインフラストラクチャの多くはサブスクリプションをサポートしていません。ゲートウェイレベルでライブクエリを追加すると、既存のインフラストラクチャに「リアルタイム」機能を追加できるようになります。もう触れたくないレガシーPHPバックエンドを持つことができます。その上にライブクエリを追加すると、フロントエンドはリアルタイムの更新を受信できるようになります。

なぜクライアント側からポーリングを行わないのかと疑問に思われるかもしれません。クライアント側のポーリングにより、サーバーへの要求が多くなる可能性があります。10.000人のクライアントが1秒間に1つのリクエストを行うと想像してみてください。これは1秒あたり10.000リクエストです。従来のPHPバックエンドはそのような負荷を処理できると思いますか?

ライブクエリはどのように役立ちますか?10.000クライアントがAPIゲートウェイに接続し、ライブクエリをサブスクライブします。ゲートウェイは、基本的に同じデータを要求しているため、すべての要求をまとめて、オリジンに対して1つの要求を行うことができます。

ライブクエリを使用すると、使用されている「ストリーム」の数に応じて、オリジンサーバーへのリクエストの数を減らすことができます。

では、クライアントにライブクエリを実装するにはどうすればよいでしょうか。

私たちの操作の1つについて、汎用クライアントの「生成された」ラッパーを見てください。

CountryWeather: (args: SubscriptionArgsWithInput<CountryWeatherInput>) =>
    hooks.useSubscriptionWithInput<CountryWeatherInput, CountryWeatherResponseData, Role>(WunderGraphContext, {
        operationName: "CountryWeather",
        isLiveQuery: true,
        requiresAuthentication: false,
    })(args)

この例を見ると、いくつかのことに気付くことができます。まず、useSubscriptionWithInputフックを使用しています。これは、少なくともクライアント側の観点からは、サブスクリプションとライブクエリを実際に区別する必要がないことを示しています。唯一の違いは、isLiveQueryフラグをに設定していることですtrue。サブスクリプションの場合、同じフックを使用していますが、isLiveQueryフラグをに設定しfalseます。

上記のサブスクリプションフックはすでに実装されているため、ライブクエリを機能させるために追加のコードは必要ありません。

デモのライブクエリページをご覧ください。お気づきかもしれませんが、この例では再び厄介なちらつきがあります。これは、サーバー側でレンダリングしていないためです。

21.ユニバーサルライブクエリ

ここで取り上げる最後の最後のパターンは、ユニバーサルライブクエリです。ユニバーサルライブクエリはサブスクリプションに似ていますが、サーバー側の観点からは単純です。サーバーの場合、サブスクリプションを開始するには、オリジンサーバーへのWebSocket接続を開き、ハンドシェイクを行い、サブスクライブする必要があります。ライブクエリで1回サブスクライブする必要がある場合は、単に1回「ポーリング」します。 、つまり、1つのリクエストを行っているだけです。したがって、ライブクエリは、少なくとも最初のリクエストでは、サブスクリプションと比較して実際に開始するのが少し速くなります。

どうすれば使用できますか?デモの例を見てみましょう。

const UniversalLiveQuery = () => {
    const data = useLiveQuery.CountryWeather({
        input: {
            code: "DE",
        },
    });
    return (
        <p>{JSON.stringify(data)}</p>
    )
}


export default withWunderGraph(UniversalLiveQuery);

これが、ドイツの首都ベルリンの気象データのストリームです。これは毎秒更新されます。

そもそも、どのようにしてデータを取得したのか疑問に思われるかもしれません。CountryWeather操作の定義を見てみましょう。

query ($capital: String! @internal $code: ID!) {
    countries_country(code: $code){
        code
        name
        capital @export(as: "capital")
        weather: _join  @transform(get: "weather_getCityByName.weather") {
            weather_getCityByName(name: $capital){
                weather {
                    temperature {
                        actual
                    }
                    summary {
                        title
                        description
                    }
                }
            }
        }
    }
}

実際には、2つの異なるサービスからのデータを結合しています。まず、countries APIを使用して、国の首都を取得します。capitalフィールドを内部$capital変数にエクスポートします。次に、この_joinフィールドを使用して、国のデータを天気APIと組み合わせます。最後に、@transformディレクティブを適用して応答を少しフラットにします。

これは通常の有効なGraphQLクエリです。ライブクエリパターンと組み合わせることで、どの国のどの首都でも天気をライブストリーミングできるようになりました。かっこいいですね。

他のすべてのパターンと同様に、これもデモで試してテストすることができます。ユニバーサルライブクエリページにアクセスして、遊んでください。

それでおしまい!終わったね!ユニバーサルで認証対応のデータフェッチフックを構築する方法を学んだことを願っています。

この投稿を終える前に、データフェッチフックを実装するための代替アプローチとツールについて見ていきたいと思います。

NextJSでのデータフェッチへの代替アプローチ

SSG(静的サイト生成)

サーバー側のレンダリングを使用することの主な欠点の1つは、サーバーがページのレンダリングを終了するまでクライアントが待機する必要があることです。ページの複雑さによっては、特にページに必要なすべてのデータをフェッチするために多くの連鎖リクエストを行う必要がある場合は、これに時間がかかることがあります。

この問題の1つの解決策は、サーバー上にページを静的に生成することです。NextJSを使用するとgetStaticProps、各ページの上部に非同期機能を実装できます。この関数はビルド時に呼び出され、ページに必要なすべてのデータをフェッチする役割を果たします。同時に、getInitialPropsまたはgetServerSideProps関数をページにアタッチしない場合、NextJSはこのページを静的であると見なします。つまり、ページをレンダリングするためにNodeJSプロセスは必要ありません。このシナリオでは、ページはコンパイル時に事前にレンダリングされ、CDNによってキャッシュできるようになります。

このレンダリング方法により、アプリケーションは非常に高速で簡単にホストできますが、欠点もあります。

1つは、静的ページはユーザー固有ではありません。これは、ビルド時にユーザーのコンテキストがないためです。ただし、これは公開ページでは問題になりません。ダッシュボードのようなユーザー固有のページをこのように使用できないというだけです。

トレードオフとして、ページを静的にレンダリングし、クライアント側でユーザー固有のコンテンツを追加することができます。ただし、最初のレンダリングの直後にページが更新されるため、これにより常にクライアントにちらつきが発生します。したがって、ユーザーの認証が必要なアプリケーションを構築している場合は、代わりにサーバー側のレンダリングを使用することをお勧めします。

静的サイト生成の2番目の欠点は、基になるデータが変更されるとコンテンツが古くなる可能性があることです。その場合は、ページを再構築することをお勧めします。ただし、ページ全体の再構築には時間がかかる場合があり、数ページだけを再構築する必要がある場合は不要な場合があります。幸いなことに、この問題には解決策があります。インクリメンタル静的再生です。

ISR(インクリメンタルスタティックリジェネレーション)

インクリメンタル静的再生を使用すると、個々のページを無効にして、オンデマンドで再レンダリングできます。これにより、静的サイトのパフォーマンス上の利点が得られますが、古いコンテンツの問題は解消されます。

とはいえ、これでも認証の問題は解決しませんが、これが静的サイト生成のすべてであるとは思いません。

私たちの側では、現在、ミューテーションの結果がISRを使用してページの再構築を自動的にトリガーできるパターンを調べています。理想的には、これは、カスタムロジックを実装しなくても、宣言的な方法で機能するものである可能性があります。

GraphQLフラグメント

サーバー側のレンダリング(およびクライアント側)で発生する可能性のある問題の1つは、コンポーネントツリーをトラバースしているときに、サーバーが相互に依存するクエリの巨大なウォーターフォールを作成しなければならない可能性があることです。子コンポーネントが親からのデータに依存している場合、N+1の問題に簡単に遭遇する可能性があります。

この場合のN+1は、ルートコンポーネントでデータの配列をフェッチし、配列アイテムごとに、子コンポーネントで追加のクエリを実行する必要があることを意味します。

この問題はGraphQLの使用に固有のものではないことに注意してください。GraphQLには実際にそれを解決するソリューションがありますが、RESTAPIにも同じ問題があります。解決策は、GraphQLフラグメントを適切にサポートするクライアントで使用することです。

GraphQLの作成者であるFacebook/Metaは、この問題の解決策を作成しました。これは、RelayClientと呼ばれます。

リレークライアントは、GraphQLフラグメントを介してコンポーネントと並べて「データ要件」を指定できるライブラリです。これがどのように見えるかの例を次に示します。

import type {UserComponent_user$key} from 'UserComponent_user.graphql';


const React = require('React');


const {graphql, useFragment} = require('react-relay');


type Props = {
  user: UserComponent_user$key,
};


function UserComponent(props: Props) {
  const data = useFragment(
    graphql`
      fragment UserComponent_user on User {
        name
        profile_picture(scale: 2) {
          uri
        }
      }
    `,
    props.user,
  );


  return (
    <>
      <h1>{data.name}</h1>
      <div>
        <img src={data.profile_picture?.uri} />
      </div>
    </>
  );
}

これがネストされたコンポーネントである場合、フラグメントを使用すると、データ要件をルートコンポーネントまで引き上げることができます。これは、ルートコンポーネントが、子コンポーネントのデータ要件定義を維持しながら、その子のデータをフェッチできることを意味します。

フラグメントは、より効率的なデータフェッチプロセスを可能にしながら、親コンポーネントと子コンポーネント間の緩い結合を可能にします。多くの開発者にとって、これがGraphQLを使用している実際の理由です。クエリ言語を使用したいのでGraphQLを使用しているのではなく、RelayClientの機能を活用したいのです。

私たちにとって、リレークライアントは素晴らしいインスピレーションの源です。実はリレーを使うのは大変だと思います。次の反復では、「フラグメントホイスト」アプローチの採用を検討していますが、目標は、リレークライアントよりも使いやすくすることです。

Reactサスペンス

Reactの世界で起こっているもう1つの開発は、ReactSuspenseの作成です。上で見たように、私たちはすでにサーバーでサスペンスを使用しています。Promiseを「スロー」することで、Promiseが解決されるまでコンポーネントのレンダリングを一時停止できます。これは、サーバーで非同期データフェッチを処理するための優れた方法です。

ただし、この手法をクライアントに適用することもできます。クライアントでSuspenseを使用すると、非常に効率的な方法で「フェッチ中にレンダリング」することができます。さらに、Suspenseをサポートするクライアントは、データフェッチフック用のより洗練されたAPIを可能にします。コンポーネント内の「ロード」または「エラー」状態を処理する代わりに、サスペンスはこれらの状態を次の「エラー境界」に「プッシュ」し、そこで処理します。このアプローチでは、「ハッピーパス」のみを処理するため、コンポーネント内のコードがはるかに読みやすくなります。

サーバーですでにSuspenseをサポートしているので、将来的にもクライアントサポートを追加することを確信できます。サスペンスと非サスペンスの両方のクライアントをサポートする最も慣用的な方法を理解したいだけです。このようにして、ユーザーは好みのプログラミングスタイルを自由に選択できます。

NextJSでのデータフェッチと認証のための代替技術

NextJSでのデータフェッチエクスペリエンスを向上させようとしているのは私たちだけではありません。したがって、他のテクノロジーと、それらが私たちが提案しているアプローチとどのように比較されるかを簡単に見てみましょう。

swr

私たちは実際にswrから多くのインスピレーションを得ています。私たちが実装したパターンを見ると、swrが優れたデータフェッチAPIを定義するのに本当に役立ったことがわかります。

私たちのアプローチがswrと異なる点がいくつかあり、言及する価値があるかもしれません。

SWRは、どのバックエンドでも使用できるため、はるかに柔軟で採用が容易です。私たちが採用したアプローチ、特に認証を処理する方法では、期待するAPIを提供するWunderGraphバックエンドも実行する必要があります。

たとえば、WunderGraphクライアントを使用している場合、バックエンドはOpenID ConnectRelyingPartyであることが期待されます。一方、swrクライアントはそのような仮定をしません。

個人的には、swrのようなライブラリを使用すると、最初にWunderGraphクライアントを使用していた場合と同様の結果が得られると個人的に信じています。認証ロジックを追加する必要があったため、現在はより多くのコードを維持しているだけです。

もう1つの大きな違いは、サーバー側のレンダリングです。WunderGraphは、認証が必要なアプリケーションをロードするときに不要なちらつきを取り除くように注意深く設計されています。swrのドキュメントによると、これは問題ではなく、ユーザーはダッシュボードにスピナーをロードしても問題ありません。

それよりもっとうまくやれると思います。コンテンツを含むすべてのコンポーネントをロードするのに15秒以上かかるSaaSダッシュボードを知っています。この期間中、ユーザーインターフェイスは、すべてのコンテンツを適切な場所に「小刻みに動かし」続けるため、まったく使用できなくなります。

ダッシュボード全体を事前にレンダリングしてから、クライアントを再水和できないのはなぜですか?HTMLが正しい方法でレンダリングされる場合、JavaScriptクライアントがロードされる前でもリンクはクリック可能である必要があります。

「バックエンド」全体がNextJSアプリケーションの「/api」ディレクトリに収まる場合は、おそらく「swr」ライブラリを使用するのが最善の選択です。NextAuthJSと組み合わせると、非常に優れた組み合わせになります。

代わりに、APIを実装するための専用サービスを構築している場合は、WunderGraphで提案しているような「バックエンド・フォー・フロントエンド」アプローチの方が、多くの反復ログアウトを移動できるため、より良い選択になる可能性があります。あなたのサービスとミドルウェアに。

NextAuthJS

NextAuthJSと言えば、NextJSアプリケーションに直接認証を追加してみませんか?ライブラリは、この問題を正確に解決するように設計されており、最小限の労力でNextJSアプリケーションに認証を追加します。

技術的な観点から、NextAuthJSはWunderGraphと同様のパターンに従います。アーキテクチャ全体に関しては、わずかな違いがあります。

アプリケーションを構築している場合、単一のWebサイトを超えて拡張することは決してないので、おそらくNextAuthJSを使用できます。ただし、複数のWebサイト、CLIツール、ネイティブアプリを使用する場合、またはバックエンドに接続する場合は、別のアプローチを使用することをお勧めします。

その理由を説明させてください。

NextAuthJSを実装する方法は、実際に認証フローの「発行者」になりつつあるということです。とはいえ、これはOpenID Connect準拠の発行者ではなく、カスタム実装です。したがって、開始するのは簡単ですが、実際には最初に多くの技術的負債を追加しています。

別のダッシュボードやCLIツールを追加したり、バックエンドをAPIに接続したいとします。OpenID Connect準拠の発行者を使用していた場合は、さまざまなシナリオに対応するフローがすでに実装されています。さらに、このOpenID Connectプロバイダーは、NextJSアプリケーションに緩く結合されているだけです。アプリケーション自体を発行者にするということは、認証フローを変更するときはいつでも、「フロントエンド」アプリケーションを再デプロイして変更する必要があることを意味します。また、pkceを使用したコードフローやデバイスフローなどの標準化された認証フローを使用することもできなくなります。

認証は、アプリケーション自体の外部で処理する必要があります。最近、Cloud IAMとのパートナーシップを発表しました。これにより、WunderGraphを依拠パーティとして使用するOpenIDConnectプロバイダーを数分でセットアップできます。

独自の認証フローを作成する必要がないように、簡単にできるようになっていることを願っています。

trpc

データフェッチレイヤーとフックは、実際にはWunderGraphとほとんど同じです。NextJSのサーバーサイドレンダリングにも同じアプローチを使用していると思います。

trpcは、WunderGraphと比較して、明らかにGraphQLとはほとんど関係がありません。認証に関する話も、WunderGraphほど完全ではありません。

そうは言っても、Alexはtrpcを構築するという素晴らしい仕事をしたと思います。WunderGraphよりも意見が少ないため、さまざまなシナリオに最適です。

私の理解では、trpcは、バックエンドとフロントエンドの両方でTypeScriptを使用する場合に最適に機能します。WunderGraphは別のパスを取ります。クライアントとサーバー間のコントラクトを定義するための一般的な中間点は、JSONスキーマを使用して定義されたJSON-RPCです。サーバータイプをクライアントにインポートするだけでなく、WunderGraphを使用してコード生成プロセスを実行する必要があります。

つまり、セットアップはもう少し複雑ですが、ターゲット環境としてTypeScriptをサポートするだけでなく、JSONoverHTTPをサポートする他の言語やランタイムもサポートできます。

その他のGraphQLクライアント

Apollo Client、urql、graphql-requestなど、他にも多くのGraphQLクライアントがあります。それらすべてに共通しているのは、通常、トランスポートとしてJSON-RPCを使用しないということです。

私はおそらく以前に複数のブログ投稿でこれを書いたことがありますが、HTTPPOSTを介して読み取り要求を送信するとインターネットが壊れます。コンパイル/トランスパイルステップを使用するすべてのアプリケーションの99%のように、GraphQLオペレーションを変更しない場合、なぜこれを行うGraphQLクライアントを使用するのでしょうか。

クライアント、ブラウザ、キャッシュサーバー、プロキシ、CDNはすべて、Cache-ControlヘッダーとETagを理解しています。人気のあるNextJSデータフェッチクライアント「swr」は、swrが「stalewhile revalidate」の略であり、効率的なキャッシュ無効化のためにETagを利用するパターンに他ならないため、その名前が付けられています。

GraphQLは、データの依存関係を定義するための優れた抽象化です。ただし、Webスケールのアプリケーションを展開する場合は、Webの既存のインフラストラクチャを活用する必要があります。これが意味することはこれです:GraphQLは開発中は素晴らしいですが、本番環境では、RESTの原則を可能な限り活用する必要があります。

概要

NextJSとReactの優れたデータフェッチフックを構築することは、一般的に課題です。また、最初から認証を考慮に入れている場合、多少異なるソリューションに到達していることも説明しました。個人的には、バックエンドとフロントエンドの両方のAPIレイヤーに認証を追加することで、よりクリーンなアプローチが可能になると信じています。考慮すべきもう1つの側面は、認証ロジックをどこに配置するかです。理想的には、自分で実装するのではなく、適切な実装に頼ることができます。OpenID Connectを発行者として、バックエンドフォーフロントエンド(BFF)の依存パーティと組み合わせることは、物事を切り離して維持するための優れた方法ですが、それでも非常に制御可能です。

私たちのBFFはまだCookieを作成して検証していますが、それは真実の源ではありません。私たちは常にKeycloakに委任しています。この設定の良いところは、Keycloakを別の実装に簡単に交換できることです。これは、具体的な実装ではなくインターフェースに依存することの利点です。

最後に、より多くの(SaaS)ダッシュボードがサーバー側のレンダリングを採用する必要があることを納得できることを願っています。NextJSとWunderGraphを使用すると、実装が非常に簡単になるため、試してみる価値があります。

繰り返しになりますが、デモを試してみたい場合は、リポジトリをご覧ください:https ://github.com/wundergraph/wundergraph-demo

ソース:https ://wundergraph.com/blog/nextjs_and_react_ssr_21_universal_data_fetching_patterns_and_best_practices

#saas #react #nextjs #restapi 

NextJS / React SSR:21のユニバーサルデータフェッチパターンとベストプラクティス
Diego  Elizondo

Diego Elizondo

1652116800

NextJS / React SSR: 21 Patrones Universales De Obtención De Datos

Un desarrollador frontend debería poder definir qué datos se necesitan para una página determinada, sin tener que preocuparse por cómo los datos llegan realmente a la interfaz.

Eso es lo que dijo un amigo mío recientemente en una discusión. ¿Por qué no hay una forma sencilla de obtener datos universales en NextJS?

Para responder a esta pregunta, echemos un vistazo a los desafíos relacionados con la obtención universal de datos en NextJS. Pero primero, ¿qué es realmente la obtención universal de datos?

Descargo de responsabilidad: Este va a ser un artículo largo y detallado. Va a cubrir mucho terreno y profundizará bastante en los detalles. Si espera un blog de marketing ligero, este artículo no es para usted.

NextObtención universal de datos de JS

Mi definición de obtención universal de datos es que puede colocar un gancho de obtención de datos en cualquier lugar de su aplicación, y simplemente funcionaría. Este gancho de obtención de datos debería funcionar en todas partes de su aplicación sin ninguna configuración adicional.

Aquí hay un ejemplo, probablemente el más complicado, pero estoy demasiado emocionado como para no compartirlo contigo.

Este es un gancho de "suscripción universal".

const PriceUpdates = () => {
    const data = useSubscription.PriceUpdates();
    return (
        <div>
            <h1>Universal Subscription</h1>
            <p>{JSON.stringify(data)}</p>
        </div>
    )
}

Nuestro marco genera el gancho "PriceUpdates" ya que hemos definido un archivo "PriceUpdates.graphql" en nuestro proyecto.

¿Qué tiene de especial este gancho? Puede colocar React Component en cualquier lugar de su aplicación. De forma predeterminada, el servidor renderizará el primer elemento de la suscripción. El HTML generado por el servidor se enviará al cliente, junto con los datos. El cliente rehidratará la aplicación e iniciará una suscripción por sí mismo.

Todo esto se hace sin ninguna configuración adicional. Funciona en todas partes de su aplicación, de ahí el nombre, obtención universal de datos. Defina los datos que necesita, escribiendo una operación GraphQL, y el marco se encargará del resto.

Tenga en cuenta que no estamos tratando de ocultar el hecho de que se están realizando llamadas de red. Lo que estamos haciendo aquí es devolverles a los desarrolladores frontend su productividad. No debería preocuparse por cómo se obtienen los datos, cómo proteger la capa API, qué transporte usar, etc. Debería funcionar.

¿Por qué es tan difícil obtener datos en NextJS?

Si ha estado usando NextJS por un tiempo, es posible que se pregunte qué debería ser exactamente difícil en la obtención de datos.

En NextJS, simplemente puede definir un punto final en el directorio "/api", al que luego se puede llamar usando "swr" o simplemente "buscar".

Es correcto que el "¡Hola, mundo!" El ejemplo de obtener datos de "/api" es realmente simple, pero escalar una aplicación más allá de la primera página puede abrumar rápidamente al desarrollador.

Veamos los principales desafíos de la obtención de datos en NextJS.

getServerSideProps solo funciona en páginas raíz

De forma predeterminada, el único lugar donde puede usar funciones asíncronas para cargar datos necesarios para la representación del lado del servidor es en la raíz de cada página.

Aquí hay un ejemplo de la documentación de NextJS:

function Page({ data }) {
  // Render data...
}


// This gets called on every request
export async function getServerSideProps() {
  // Fetch data from external API
  const res = await fetch(`https://.../data`)
  const data = await res.json()


  // Pass data to the page via props
  return { props: { data } }
}


export default Page

Imagine un sitio web con cientos de páginas y componentes. Si tiene que definir todas las dependencias de datos en la raíz de cada página, ¿cómo sabe qué datos se necesitan realmente antes de representar el árbol de componentes? Dependiendo de los datos que haya cargado para los componentes raíz, alguna lógica podría decidir cambiar completamente los componentes secundarios.

He hablado con desarrolladores que tienen que mantener grandes aplicaciones de NextJS. Han declarado claramente que la obtención de datos en "getServerSideProps" no se escala bien con una gran cantidad de páginas y componentes.

La autenticación agrega complejidad adicional a la obtención de datos

La mayoría de las aplicaciones tienen algún tipo de mecanismo de autenticación. Puede haber algún contenido que esté disponible públicamente, pero ¿qué sucede si desea personalizar un sitio web?

Habrá una necesidad de renderizar diferentes contenidos para diferentes usuarios.

Cuando presenta contenido específico del usuario solo en el cliente, ¿ha notado este feo efecto de "parpadeo" una vez que ingresan los datos?

Si solo está representando el contenido específico del usuario en el cliente, siempre obtendrá el efecto de que la página se volverá a representar varias veces hasta que esté lista.

Idealmente, nuestros ganchos de obtención de datos serían conscientes de la autenticación desde el primer momento.

Se necesita Type-Safety para evitar errores y hacer que los desarrolladores sean productivos

Como hemos visto en el ejemplo anterior usando "getServerSideProps", necesitamos tomar acciones adicionales para hacer que nuestra capa de API sea segura. ¿No sería mejor si los ganchos de obtención de datos fueran de tipo seguro por defecto?

Las suscripciones no se pueden procesar en el servidor, ¿verdad?

Hasta ahora, nunca he visto a nadie que haya aplicado renderizado del lado del servidor en NextJS a las suscripciones. Pero, ¿qué sucede si desea representar el precio de las acciones en el servidor por razones de rendimiento y SEO, pero también desea tener una suscripción del lado del cliente para recibir actualizaciones?

Seguramente, podría usar una solicitud Query/GET en el servidor y luego agregar una suscripción en el cliente, pero esto agrega mucha complejidad. ¡Debería haber una manera más simple!

¿Qué debería pasar si el usuario sale y vuelve a entrar en la ventana?

Otra pregunta que surge es qué debería pasar si el usuario sale y vuelve a entrar en la ventana. ¿Deberían detenerse las suscripciones o continuar transmitiendo datos? Según el caso de uso y el tipo de aplicación, es posible que desee modificar este comportamiento, según la experiencia esperada del usuario y el tipo de datos que está obteniendo. Nuestros ganchos de obtención de datos deberían poder manejar esto.

¿Deberían las mutaciones afectar a otros ganchos de obtención de datos?

Es bastante común que las mutaciones tengan efectos secundarios en otros ganchos de obtención de datos. Por ejemplo, podría tener una lista de tareas. Cuando agrega una nueva tarea, también desea actualizar la lista de tareas. Por lo tanto, los ganchos de obtención de datos deben poder manejar este tipo de situaciones.

¿Qué pasa con la carga diferida? #

Otro patrón común es la carga diferida. Es posible que desee cargar datos solo en determinadas condiciones, por ejemplo, cuando el usuario se desplaza hasta la parte inferior de la página o cuando hace clic en un botón. En tales casos, nuestros ganchos de obtención de datos deberían poder diferir la ejecución de la obtención hasta que realmente se necesiten los datos.

¿Cómo podemos bloquear la ejecución de una consulta cuando el usuario escribe un término de búsqueda? #

Otro requisito importante para los ganchos de obtención de datos es eliminar el rebote de la ejecución de una consulta. Esto es para evitar solicitudes innecesarias al servidor. Imagine una situación en la que un usuario escribe un término de búsqueda en un cuadro de búsqueda. ¿Realmente debería hacer una solicitud al servidor cada vez que el usuario escribe una carta? Veremos cómo podemos usar el antirrebote para evitar esto y hacer que nuestros ganchos de obtención de datos sean más eficaces.

Resumen de los mayores desafíos de construir ganchos de obtención de datos para NextJS #

  1. getServerSideProps solo funciona en páginas raíz
  2. ganchos de obtención de datos con reconocimiento de autenticación
  3. tipo de seguridad
  4. suscripciones y SSR
  5. enfoque y desenfoque de ventana
  6. efectos secundarios de las mutaciones
  7. carga lenta
  8. despegar

Eso nos lleva a 8 problemas centrales que debemos resolver. Analicemos ahora 21 patrones y mejores prácticas para resolver estos problemas.

21 patrones y mejores prácticas Resolviendo los 8 problemas centrales de los ganchos de obtención de datos para NextJS

Si desea seguir y experimentar estos patrones usted mismo, puede clonar este repositorio y jugar . Para cada patrón, hay una página dedicada en la demostración .

Una vez que haya iniciado la demostración, puede abrir su navegador y encontrar la descripción general de los patrones en http://localhost:3000/patterns.

Notará que estamos usando GraphQL para definir nuestros ganchos de obtención de datos, pero la implementación realmente no es específica de GraphQL. Puede aplicar los mismos patrones con otros estilos de API como REST, o incluso con una API personalizada.

1. Usuario del lado del cliente

El primer patrón que veremos es el usuario del lado del cliente, es la base para construir ganchos de obtención de datos con reconocimiento de autenticación.

Aquí está el gancho para buscar al usuario actual:

useEffect(() => {
        if (disableFetchUserClientSide) {
            return;
        }
        const abort = new AbortController();
        if (user === null) {
            (async () => {
                try {
                    const nextUser = await ctx.client.fetchUser(abort.signal);
                    if (JSON.stringify(nextUser) === JSON.stringify(user)) {
                        return;
                    }
                    setUser(nextUser);
                } catch (e) {
                }
            })();
        }
        return () => {
            abort.abort();
        };
    }, [disableFetchUserClientSide]);

Dentro de la raíz de nuestra página, usaremos este enlace para obtener el usuario actual (si aún no se obtuvo en el servidor). Es importante pasar siempre el controlador de cancelación al cliente, de lo contrario, podríamos tener pérdidas de memoria. La función de flecha de retorno se llama cuando se desmonta el componente que contiene el gancho.

Notará que estamos usando este patrón en toda nuestra aplicación para manejar las posibles fugas de memoria de manera adecuada.

Veamos ahora la implementación de "client.fetchUser".

public fetchUser = async (abortSignal?: AbortSignal, revalidate?: boolean): Promise<User<Role> | null> => {
    try {
        const revalidateTrailer = revalidate === undefined ? "" : "?revalidate=true";
        const response = await fetch(this.baseURL + "/" + this.applicationPath + "/auth/cookie/user" + revalidateTrailer, {
            headers: {
                ...this.extraHeaders,
                "Content-Type": "application/json",
                "WG-SDK-Version": this.sdkVersion,
            },
            method: "GET",
            credentials: "include",
            mode: "cors",
            signal: abortSignal,
        });
        if (response.status === 200) {
            return response.json();
        }
    } catch {
    }
    return null;
};

Notará que no estamos enviando ninguna credencial de cliente, token o cualquier otra cosa. Implícitamente enviamos la cookie segura, encriptada y solo http que configuró el servidor, a la que nuestro cliente no tiene acceso.

Para aquellos que no lo saben, las cookies de solo http se adjuntan automáticamente a cada solicitud si se encuentra en el mismo dominio. Si está utilizando HTTP/2, también es posible que el cliente y el servidor apliquen compresión de encabezado, lo que significa que la cookie no tiene que enviarse en cada solicitud, ya que tanto el cliente como el servidor pueden negociar un mapa de valor de clave de encabezado conocido. pares en el nivel de conexión.

El patrón que estamos usando detrás de escena para hacer que la autenticación sea tan simple se llama "Patrón de controlador de token". El patrón del controlador de tokens es la forma más segura de manejar la autenticación en las aplicaciones JavaScript modernas. Si bien es muy seguro, también nos permite ser independientes del proveedor de identidad.

Al aplicar el patrón del controlador de tokens, podemos cambiar fácilmente entre diferentes proveedores de identidad. Esto se debe a que nuestro "backend" actúa como una parte dependiente de OpenID Connect.

¿Qué es una parte dependiente, podrías preguntar? Es una aplicación con un cliente OpenID Connect que externaliza la autenticación a un tercero. Como estamos hablando en el contexto de OpenID Connect, nuestro "backend" es compatible con cualquier servicio que implemente el protocolo OpenID Connect. De esta forma, nuestro backend puede brindar una experiencia de autenticación perfecta, mientras que los desarrolladores pueden elegir entre diferentes proveedores de identidad, como Keycloak, Auth0, Okta, Ping Identity, etc.

¿Cómo se ve el flujo de autenticación desde la perspectiva de los usuarios?

  1. el usuario hace clic en iniciar sesión
  2. el frontend redirige al usuario al backend (parte de confianza)
  3. el backend redirige al usuario al proveedor de identidad
  4. el usuario se autentica en el proveedor de identidad
  5. si la autenticación es exitosa, el proveedor de identidad redirige al usuario al backend
  6. el backend luego intercambia el código de autorización por un token de acceso e identidad
  7. el token de acceso e identidad se utiliza para establecer una cookie segura, encriptada y solo http en el cliente
  8. con el conjunto de cookies, el usuario es redirigido de nuevo a la interfaz

A partir de ahora, cuando el cliente llame al fetchUsermétodo, enviará automáticamente la cookie al backend. De esta manera, la interfaz siempre tiene acceso a la información del usuario mientras está conectado.

Si el usuario hace clic en cerrar sesión, llamaremos a una función en el backend que invalidará la cookie.

Todo esto puede ser mucho para digerir, así que resumamos las partes esenciales. Primero, debe decirle al backend con qué proveedores de identidad trabajar para que pueda actuar como Reyling Party. Una vez hecho esto, podrá iniciar el flujo de autenticación desde el frontend, obtener al usuario actual del backend y cerrar la sesión.

Si envolvemos esta llamada "fetchUser" en un enlace useEffectque colocamos en la raíz de cada página, siempre sabremos cuál es el usuario actual.

Sin embargo, hay una trampa. Si abre la demostración y se dirige a la página de usuario del lado del cliente , notará que hay un efecto de parpadeo después de cargar la página, eso se debe a que la fetchUserllamada se está realizando en el cliente.

Si observa Chrome DevTools y abre la vista previa de la página, notará que la página se representa con el objeto de usuario establecido en null. Puede hacer clic en el botón de inicio de sesión para iniciar el flujo de inicio de sesión. Una vez completado, actualice la página y verá el efecto de parpadeo.

Ahora que comprende la mecánica detrás del patrón del controlador de fichas, echemos un vistazo a cómo podemos eliminar el parpadeo en la carga de la primera página.

2. Usuario del lado del servidor

Si desea deshacerse del parpadeo, tenemos que cargar al usuario en el lado del servidor para que pueda aplicar la representación del lado del servidor. Al mismo tiempo, tenemos que llevar de alguna manera el usuario renderizado del lado del servidor al cliente. Si omitimos ese segundo paso, la rehidratación del cliente fallará ya que el html generado por el servidor diferirá del primer procesamiento del lado del cliente.

Entonces, ¿cómo obtenemos acceso al objeto de usuario en el lado del servidor? Recuerde que todo lo que tenemos es una cookie adjunta a un dominio.

Digamos que nuestro backend se ejecuta en api.example.com, y el frontend se ejecuta en www.example.como example.com.

Si hay algo importante que debe saber sobre las cookies es que puede establecer cookies en los dominios principales si está en un subdominio. Esto significa que, una vez que se completa el flujo de autenticación, el backend NO debe establecer la cookie en el api.example.comdominio. En su lugar, debería establecer la cookie en el example.comdominio. Al hacerlo, la cookie se vuelve visible para todos los subdominios de example.com, incluido www.example.com, api.example.comy para example.comella misma.

Por cierto, este es un patrón excelente para implementar el inicio de sesión único. Haga que sus usuarios inicien sesión una vez y se autentican en todos los subdominios.

WunderGraph establece automáticamente las cookies en el dominio principal si el backend está en un subdominio, por lo que no tiene que preocuparse por esto.

Ahora, volvamos a poner al usuario en el lado del servidor. Para llevar al usuario del lado del servidor, tenemos que implementar alguna lógica en el getInitialPropsmétodo de nuestras páginas.

WunderGraphPage.getInitialProps = async (ctx: NextPageContext) => {


// ... omitted for brevity


const cookieHeader = ctx.req?.headers.cookie;
if (typeof cookieHeader === "string") {
    defaultContextProperties.client.setExtraHeaders({
        Cookie: cookieHeader,
    });
}


let ssrUser: User<Role> | null = null;


if (options?.disableFetchUserServerSide !== true) {
    try {
        ssrUser = await defaultContextProperties.client.fetchUser();
    } catch (e) {
    }
}


// ... omitted for brevity
return {...pageProps, ssrCache, user: ssrUser};

El ctxobjeto de la getInitialPropsfunción contiene la solicitud del cliente, incluidos los encabezados. Podemos hacer un "truco de magia" para que el "cliente API", que creamos en el lado del servidor, pueda actuar en nombre del usuario.

Como tanto el frontend como el backend comparten el mismo dominio principal, tenemos acceso a la cookie que configuró el backend. Entonces, si tomamos el encabezado de la cookie y lo configuramos como el Cookieencabezado del cliente API, el cliente API podrá actuar en el contexto del usuario, ¡incluso en el lado del servidor!

Ahora podemos buscar al usuario en el lado del servidor y pasar el objeto de usuario junto con los pageProps a la función de representación de la página. Asegúrese de no perder este último paso, de lo contrario la rehidratación del cliente fallará.

Muy bien, hemos resuelto el problema del parpadeo, al menos cuando presionas actualizar. Pero, ¿qué sucede si comenzamos en una página diferente y usamos la navegación del lado del cliente para llegar a esta página?

Abra la demostración y pruébelo usted mismo. Verá que el objeto de usuario se establecerá en nullsi el usuario no se cargó en la otra página.

Para resolver también este problema, tenemos que ir un paso más allá y aplicar el patrón de "usuario universal".

3. Usuario universal

El patrón de usuario universal es la combinación de los dos patrones anteriores.

Si estamos accediendo a la página por primera vez, cargue al usuario en el lado del servidor, si es posible, y renderice la página. En el lado del cliente, rehidratamos la página con el objeto de usuario y no lo recuperamos, por lo tanto, no hay parpadeo.

En el segundo escenario, estamos usando la navegación del lado del cliente para llegar a nuestra página. En este caso, comprobamos si el usuario ya está cargado. Si el objeto de usuario es nulo, intentaremos recuperarlo.

¡Genial, tenemos el patrón de usuario universal en su lugar! Pero hay otro problema que podríamos enfrentar. ¿Qué sucede si el usuario abre una segunda pestaña o ventana y hace clic en el botón de cierre de sesión?

Abra la página de usuario universal en la demostración en dos pestañas o ventanas y pruébelo usted mismo. Si hace clic en cerrar sesión en una pestaña, luego regresa a la otra pestaña, verá que el objeto de usuario todavía está allí.

El patrón "recuperar usuario en el foco de la ventana" es una solución a este problema.

4. Recupere el usuario en el enfoque de la ventana

Afortunadamente, podemos usar el window.addEventListenermétodo para escuchar el focusevento. De esta manera, recibimos una notificación cada vez que el usuario activa la pestaña o ventana.

Agreguemos un gancho a nuestra página para manejar eventos de ventana.

const windowHooks = (setIsWindowFocused: Dispatch<SetStateAction<"pristine" | "focused" | "blurred">>) => {
    useEffect(() => {
        const onFocus = () => {
            setIsWindowFocused("focused");
        };
        const onBlur = () => {
            setIsWindowFocused("blurred");
        };
        window.addEventListener('focus', onFocus);
        window.addEventListener('blur', onBlur);
        return () => {
            window.removeEventListener('focus', onFocus);
            window.removeEventListener('blur', onBlur);
        };
    }, []);
}

Notará que presentamos tres estados posibles para la acción "isWindowFocused": prístina, enfocada y borrosa. ¿Por qué tres estados? Imagina si tuviéramos solo dos estados, enfocado y borroso. En este caso, siempre tendríamos que disparar un evento de "enfoque", incluso si la ventana ya estaba enfocada. Al introducir el tercer estado (prístino), podemos evitar esto.

Otra observación importante que puede hacer es que estamos eliminando los detectores de eventos cuando se desmonta el componente. Esto es muy importante para evitar pérdidas de memoria.

Ok, hemos introducido un estado global para el foco de la ventana. Aprovechemos este estado para volver a buscar al usuario en el foco de la ventana agregando otro enlace:

useEffect(() => {
    if (disableFetchUserClientSide) {
        return;
    }
    if (disableFetchUserOnWindowFocus) {
        return;
    }
    if (isWindowFocused !== "focused") {
        return
    }
    const abort = new AbortController();
    (async () => {
        try {
            const nextUser = await ctx.client.fetchUser(abort.signal);
            if (JSON.stringify(nextUser) === JSON.stringify(user)) {
                return;
            }
            setUser(nextUser);
        } catch (e) {
        }
    })();
    return () => {
        abort.abort();
    };
}, [isWindowFocused, disableFetchUserClientSide, disableFetchUserOnWindowFocus]);

Al agregar el isWindowFocusedestado a la lista de dependencias, este efecto se activará cada vez que cambie el enfoque de la ventana. Descartamos los eventos "prístinos" y "borrosos" y solo activamos una búsqueda de usuario si la ventana está enfocada.

Además, nos aseguramos de que solo activemos un estado setState para el usuario si realmente cambió. De lo contrario, podríamos activar renderizaciones o recuperaciones innecesarias.

¡Excelente! Nuestra aplicación ahora puede manejar la autenticación en varios escenarios. Esa es una gran base para pasar a los ganchos reales de obtención de datos.

5. Consulta del lado del cliente

El primer gancho de obtención de datos que veremos es la consulta del lado del cliente . Puede abrir la página de demostración (http://localhost:3000/patterns/client-side-query) en su navegador para familiarizarse con ella.

const data = useQuery.CountryWeather({
    input: {
        code: "DE",
    },
});

Entonces, ¿qué hay detrás useQuery.CountryWeather? ¡Echemos un vistazo!

function useQueryContextWrapper<Input, Data, Role>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, query: QueryProps, args?: InternalQueryArgsWithInput<Input>): {
    result: QueryResult<Data>;
} {
    const {client} = useContext(wunderGraphContext);
    const cacheKey = client.cacheKey(query, args);
    const [statefulArgs, setStatefulArgs] = useState<InternalQueryArgsWithInput<Input> | undefined>(args);
    const [queryResult, setQueryResult] = useState<QueryResult<Data> | undefined>({status: "none"});
    useEffect(() => {
        if (lastCacheKey === "") {
            setLastCacheKey(cacheKey);
            return;
        }
        if (lastCacheKey === cacheKey) {
            return;
        }
        setLastCacheKey(cacheKey);
        setStatefulArgs(args);
        setInvalidate(invalidate + 1);
    }, [cacheKey]);
    useEffect(() => {
       const abort = new AbortController();
        setQueryResult({status: "loading"});
        (async () => {
            const result = await client.query(query, {
                ...statefulArgs,
                abortSignal: abort.signal,
            });
            setQueryResult(result as QueryResult<Data>);
        })();
        return () => {
            abort.abort();
            setQueryResult({status: "cancelled"});
        }
    }, [invalidate]);
    return {
        result: queryResult as QueryResult<Data>,
    }
}

Vamos a explicar lo que está pasando aquí. Primero, tomamos el cliente que se está inyectando a través de React.Context. Luego calculamos una clave de caché para la consulta y los argumentos. Esta cacheKey nos ayuda a determinar si necesitamos volver a obtener los datos.

El estado inicial de la operación se establece en {status: "none"}. Cuando se activa la primera obtención, el estado se establece en "loading". Cuando finaliza la búsqueda, el estado se establece en "success"o "error". Si el componente que envuelve este gancho se está desmontando, el estado se establece en "cancelled".

Aparte de eso, nada especial está sucediendo aquí. La recuperación solo ocurre cuando se activa useEffect. Esto significa que no podemos ejecutar la búsqueda en el servidor. React.Hooks no se ejecuta en el servidor.

Si observa la demostración, notará que vuelve a parpadear. Esto se debe a que no estamos procesando el componente en el servidor. ¡Mejoremos esto!

6. Consulta del lado del servidor

Para ejecutar consultas no solo en el cliente sino también en el servidor, debemos aplicar algunos cambios a nuestros ganchos.

Primero actualicemos el useQuerygancho.

function useQueryContextWrapper<Input, Data, Role>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, query: QueryProps, args?: InternalQueryArgsWithInput<Input>): {
    result: QueryResult<Data>;
} {
    const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
    const isServer = typeof window === 'undefined';
    const ssrEnabled = args?.disableSSR !== true && args?.lazy !== true;
    const cacheKey = client.cacheKey(query, args);
    if (isServer) {
        if (ssrEnabled) {
            if (ssrCache[cacheKey]) {
                return {
                    result: ssrCache[cacheKey] as QueryResult<Data>,
                }
            }
            const promise = client.query(query, args);
            ssrCache[cacheKey] = promise;
            throw promise;
        } else {
            ssrCache[cacheKey] = {
                status: "none",
            };
            return {
                result: ssrCache[cacheKey] as QueryResult<Data>,
            }
        }
    }
    const [invalidate, setInvalidate] = useState<number>(0);
    const [statefulArgs, setStatefulArgs] = useState<InternalQueryArgsWithInput<Input> | undefined>(args);
    const [lastCacheKey, setLastCacheKey] = useState<string>("");
    const [queryResult, setQueryResult] = useState<QueryResult<Data> | undefined>(ssrCache[cacheKey] as QueryResult<Data> || {status: "none"});
    useEffect(() => {
        if (lastCacheKey === "") {
            setLastCacheKey(cacheKey);
            return;
        }
        if (lastCacheKey === cacheKey) {
            return;
        }
        setLastCacheKey(cacheKey);
        setStatefulArgs(args);
        if (args?.debounceMillis !== undefined) {
            setDebounce(prev => prev + 1);
            return;
        }
        setInvalidate(invalidate + 1);
    }, [cacheKey]);
    useEffect(() => {
        setQueryResult({status: "loading"});
        (async () => {
            const result = await client.query(query, {
                ...statefulArgs,
                abortSignal: abort.signal,
            });
            setQueryResult(result as QueryResult<Data>);
        })();
        return () => {
            abort.abort();
            setQueryResult({status: "cancelled"});
        }
    }, [invalidate]);
    return {
        result: queryResult as QueryResult<Data>,
    }
}

Ahora hemos actualizado el enlace useQuery para verificar si estamos en el servidor o no. Si estamos en el servidor, verificaremos si los datos ya se resolvieron para la clave de caché generada. Si los datos fueron resueltos, los devolveremos. De lo contrario, usaremos el cliente para ejecutar la consulta mediante una Promesa. Pero hay un problema. No se nos permite ejecutar código asíncrono mientras se renderiza en el servidor. Entonces, en teoría, no podemos "esperar" a que se resuelva la promesa.

En cambio, tenemos que usar un truco. Necesitamos "suspender" el renderizado. Podemos hacerlo "lanzando" la promesa que acabamos de crear.

Imagine que estamos renderizando el componente envolvente en el servidor. Lo que podríamos hacer es envolver el proceso de renderizado de cada componente en un bloque try/catch. Si uno de esos componentes arroja una promesa, podemos capturarlo, esperar hasta que se resuelva la promesa y luego volver a procesar el componente.

Una vez que se resuelve la promesa, podemos llenar la clave de caché con el resultado. De esta manera, podemos devolver los datos inmediatamente cuando "intentamos" renderizar el componente por segunda vez. Con este método, podemos movernos por el árbol de componentes y ejecutar todas las consultas que están habilitadas para la representación del lado del servidor.

Quizás se pregunte cómo implementar este método de prueba/captura. Por suerte, no tenemos que empezar de cero. Hay una biblioteca llamada react-ssr-prepass que podemos usar para hacer esto.

Apliquemos esto a nuestra getInitialPropsfunción:

WithWunderGraph.getInitialProps = async (ctx: NextPageContext) => {


    const pageProps = (Page as NextPage).getInitialProps ? await (Page as NextPage).getInitialProps!(ctx as any) : {};
    const ssrCache: { [key: string]: any } = {};


    if (typeof window !== 'undefined') {
        // we're on the client
        // no need to do all the SSR stuff
        return {...pageProps, ssrCache};
    }


    const cookieHeader = ctx.req?.headers.cookie;
    if (typeof cookieHeader === "string") {
        defaultContextProperties.client.setExtraHeaders({
            Cookie: cookieHeader,
        });
    }


    let ssrUser: User<Role> | null = null;


    if (options?.disableFetchUserServerSide !== true) {
        try {
            ssrUser = await defaultContextProperties.client.fetchUser();
        } catch (e) {
        }
    }


    const AppTree = ctx.AppTree;


    const App = createElement(wunderGraphContext.Provider, {
        value: {
            ...defaultContextProperties,
            user: ssrUser,
        },
    }, createElement(AppTree, {
        pageProps: {
            ...pageProps,
        },
        ssrCache,
        user: ssrUser
    }));


    await ssrPrepass(App);
    const keys = Object.keys(ssrCache).filter(key => typeof ssrCache[key].then === 'function').map(key => ({
        key,
        value: ssrCache[key]
    })) as { key: string, value: Promise<any> }[];
    if (keys.length !== 0) {
        const promises = keys.map(key => key.value);
        const results = await Promise.all(promises);
        for (let i = 0; i < keys.length; i++) {
            const key = keys[i].key;
            ssrCache[key] = results[i];
        }
    }


    return {...pageProps, ssrCache, user: ssrUser};
};

El ctxobjeto no solo contiene el reqobjeto sino también los AppTreeobjetos. Usando el AppTreeobjeto, podemos construir todo el árbol de componentes e inyectar nuestro proveedor de contexto, el ssrCacheobjeto y el userobjeto.

Luego podemos usar la ssrPrepassfunción para atravesar el árbol de componentes y ejecutar todas las consultas que están habilitadas para la representación del lado del servidor. Después de hacerlo, extraemos los resultados de todas las Promesas y completamos el ssrCacheobjeto. Finalmente, devolvemos el pagePropsobjeto y el ssrCacheobjeto así como el userobjeto.

¡Fantástico! ¡Ahora podemos aplicar la representación del lado del servidor a nuestro enlace useQuery!

Vale la pena mencionar que hemos desvinculado por completo la representación del lado del servidor de tener que implementarla getServerSidePropsen nuestro Pagecomponente. Esto tiene algunos efectos que es importante discutir.

Primero, hemos resuelto el problema de que tenemos que declarar nuestras dependencias de datos en getServerSideProps. Somos libres de colocar nuestros ganchos useQuery en cualquier parte del árbol de componentes, siempre se ejecutarán.

Por otro lado, este enfoque tiene la desventaja de que esta página no estará optimizada estáticamente. En su lugar, la página siempre se procesará en el servidor, lo que significa que debe haber un servidor ejecutándose para servir la página. Otro enfoque sería crear una página renderizada estáticamente, que se puede servir completamente desde un CDN.

Dicho esto, asumimos en esta guía que su objetivo es ofrecer contenido dinámico que cambia según el usuario. En este escenario, la representación estática de la página no será una opción, ya que no tenemos ningún contexto de usuario al obtener los datos.

Es genial lo que hemos logrado hasta ahora. Pero, ¿qué debería pasar si el usuario deja la ventana por un tiempo y vuelve? ¿Es posible que los datos que hemos obtenido en el pasado estén desactualizados? Si es así, ¿cómo podemos hacer frente a esta situación? ¡Al siguiente patrón!

7. Consulta de recuperación en el foco de la ventana

Afortunadamente, ya implementamos un objeto de contexto global para propagar los tres estados de enfoque de ventana diferentes: prístino, borroso y enfocado.

Aprovechemos el estado "enfocado" para activar una recuperación de la consulta.

Recuerde que estábamos usando el contador "invalidar" para activar una recuperación de la consulta. Podemos agregar un nuevo efecto para aumentar este contador siempre que la ventana esté enfocada.

useEffect(() => {
    if (!refetchOnWindowFocus) {
        return;
    }
    if (isWindowFocused !== "focused") {
        return;
    }
    setInvalidate(prev => prev + 1);
}, [refetchOnWindowFocus, isWindowFocused]);

¡Eso es todo! Descartamos todos los eventos si refetchOnWindowFocus se establece en falso o si la ventana no está enfocada. De lo contrario, aumentamos el contador de invalidaciones y activamos una nueva búsqueda de la consulta.

Si está siguiendo la demostración, eche un vistazo a la página de refetch-query-on-window-focus .

El enlace, incluida la configuración, se ve así:

const data = useQuery.CountryWeather({
    input: {
        code: "DE",
    },
    disableSSR: true,
    refetchOnWindowFocus: true,
});

¡Eso fue rápido! Pasemos al siguiente patrón, carga diferida.

8. Consulta perezosa

Como se discutió en el enunciado del problema, algunas de nuestras operaciones deben ejecutarse solo después de un evento específico. Hasta entonces, la ejecución debe ser aplazada.

Echemos un vistazo a la página de consulta diferida .

const [args,setArgs] = useState<QueryArgsWithInput<CountryWeatherInput>>({
    input: {
        code: "DE",
    },
    lazy: true,
});

Establecer perezoso en verdadero configura el gancho para que sea "perezoso". Ahora, veamos la implementación:

useEffect(() => {
    if (lazy && invalidate === 0) {
        setQueryResult({
            status: "lazy",
        });
        return;
    }
    const abort = new AbortController();
    setQueryResult({status: "loading"});
    (async () => {
        const result = await client.query(query, {
            ...statefulArgs,
            abortSignal: abort.signal,
        });
        setQueryResult(result as QueryResult<Data>);
    })();
    return () => {
        abort.abort();
        setQueryResult({status: "cancelled"});
    }
}, [invalidate]);
const refetch = useCallback((args?: InternalQueryArgsWithInput<Input>) => {
    if (args !== undefined) {
        setStatefulArgs(args);
    }
    setInvalidate(prev => prev + 1);
}, []);

Cuando este hook se ejecuta por primera vez, lazy se establecerá en true y invalidate se establecerá en 0. Esto significa que el hook de efecto regresará temprano y establecerá el resultado de la consulta en "lazy". No se ejecuta una búsqueda en este escenario.

Si queremos ejecutar la consulta, tenemos que aumentar la invalidación en 1. Podemos hacerlo llamando refetchal gancho useQuery.

¡Eso es todo! La carga diferida ahora está implementada.

Pasemos al siguiente problema: eliminar las entradas de los usuarios para no obtener la consulta con demasiada frecuencia.

9. Consulta de rebote

Digamos que el usuario quiere obtener el clima de una ciudad específica. Mi ciudad natal es "Frankfurt am Main", justo en el centro de Alemania. Ese término de búsqueda tiene 17 caracteres. ¿Con qué frecuencia debemos obtener la consulta mientras el usuario está escribiendo? 17 veces? ¿Una vez? ¿Quizás dos veces?

La respuesta estará en algún punto intermedio, pero definitivamente no es 17 veces. Entonces, ¿cómo podemos implementar este comportamiento? Echemos un vistazo a la implementación del gancho useQuery.

useEffect(() => {
    if (debounce === 0) {
        return;
    }
    const cancel = setTimeout(() => {
        setInvalidate(prev => prev + 1);
    }, args?.debounceMillis || 0);
    return () => clearTimeout(cancel);
}, [debounce]);
useEffect(() => {
    if (lastCacheKey === "") {
        setLastCacheKey(cacheKey);
        return;
    }
    if (lastCacheKey === cacheKey) {
        return;
    }
    setLastCacheKey(cacheKey);
    setStatefulArgs(args);
    if (args?.debounceMillis !== undefined) {
        setDebounce(prev => prev + 1);
        return;
    }
    setInvalidate(invalidate + 1);
}, [cacheKey]);

Primero echemos un vistazo al segundo useEffect, el que tiene cacheKey como dependencia. Puede ver que antes de aumentar el contador de invalidaciones, verificamos si los argumentos de la operación contienen una propiedad debounceMillis. Si es así, no aumentamos inmediatamente el contador de invalidaciones. En su lugar, aumentamos el contador de rebotes.

Aumentar el contador de rebote activará el primer useEffect, ya que el contador de rebote es una dependencia. Si el contador de rebotes es 0, que es el valor inicial, regresamos inmediatamente, ya que no hay nada que hacer. De lo contrario, iniciamos un temporizador usando setTimeout. Una vez que se activa el tiempo de espera, aumentamos el contador de invalidaciones.

Lo especial del efecto que usa setTimeout es que estamos aprovechando la función de retorno del gancho del efecto para borrar el tiempo de espera. Lo que esto significa es que si el usuario escribe más rápido que el tiempo de rebote, el temporizador siempre se borra y el contador de invalidaciones no aumenta. Solo cuando ha pasado el tiempo completo de rebote, se incrementa el contador de invalidaciones.

Veo a menudo que los desarrolladores usan setTimeout pero se olvidan de manejar el objeto que regresa. No manejar el valor de retorno de setTimeout podría provocar pérdidas de memoria, ya que también es posible que el componente React adjunto se desmonte antes de que se active el tiempo de espera.

Si está interesado en jugar, diríjase a la demostración e intente escribir diferentes términos de búsqueda utilizando varios tiempos de rebote.

¡Estupendo! Tenemos una buena solución para contrarrestar las entradas de los usuarios. Veamos ahora las operaciones que requieren que el usuario esté autenticado. Comenzaremos con una consulta protegida del lado del servidor.

10. Consulta protegida del lado del servidor

Digamos que estamos representando un tablero que requiere que el usuario esté autenticado. El tablero también mostrará datos específicos del usuario. ¿Cómo podemos implementar esto? Nuevamente, tenemos que modificar el gancho useQuery.

const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
const isServer = typeof window === 'undefined';
const ssrEnabled = args?.disableSSR !== true && args?.lazy !== true;
const cacheKey = client.cacheKey(query, args);
if (isServer) {
    if (query.requiresAuthentication && user === null) {
        ssrCache[cacheKey] = {
            status: "requires_authentication"
        };
        return {
            result: ssrCache[cacheKey] as QueryResult<Data>,
            refetch: () => {
            },
        };
    }
    if (ssrEnabled) {
        if (ssrCache[cacheKey]) {
            return {
                result: ssrCache[cacheKey] as QueryResult<Data>,
                refetch: () => Promise.resolve(ssrCache[cacheKey] as QueryResult<Data>),
            }
        }
        const promise = client.query(query, args);
        ssrCache[cacheKey] = promise;
        throw promise;
    } else {
        ssrCache[cacheKey] = {
            status: "none",
        };
        return {
            result: ssrCache[cacheKey] as QueryResult<Data>,
            refetch: () => ({}),
        }
    }
}

Como discutimos en el patrón 2, Usuario del lado del servidor, ya implementamos alguna lógica para obtener el objeto del usuario getInitialPropse inyectarlo en el contexto. También inyectamos la cookie de usuario en el cliente, que también se inyecta en el contexto. Juntos, estamos listos para implementar la consulta protegida del lado del servidor.

Si estamos en el servidor, comprobamos si la consulta requiere autenticación. Esta es información estática que se define en los metadatos de la consulta. Si el objeto de usuario es nulo, lo que significa que el usuario no está autenticado, devolvemos un resultado con el estado "requires_authentication". De lo contrario, avanzamos y lanzamos una promesa o devolvemos el resultado del caché.

Si va a la consulta protegida del lado del servidor en la demostración, puede jugar con esta implementación y ver cómo se comporta cuando inicia y cierra sesión.

Eso es todo, sin magia. Eso no fue demasiado complicado, ¿verdad? Bueno, el servidor no permite ganchos, lo que hace que la lógica sea mucho más fácil. Veamos ahora lo que se requiere para implementar la misma lógica en el cliente.

11. Consulta protegida del lado del cliente #

Para implementar la misma lógica para el cliente, necesitamos modificar el enlace useQuery una vez más.

useEffect(() => {
    if (query.requiresAuthentication && user === null) {
        setQueryResult({
            status: "requires_authentication",
        });
        return;
    }
    if (lazy && invalidate === 0) {
        setQueryResult({
            status: "lazy",
        });
        return;
    }
    const abort = new AbortController();
    if (queryResult?.status === "ok") {
        setQueryResult({...queryResult, refetching: true});
    } else {
        setQueryResult({status: "loading"});
    }
    (async () => {
        const result = await client.query(query, {
            ...statefulArgs,
            abortSignal: abort.signal,
        });
        setQueryResult(result as QueryResult<Data>);
    })();
    return () => {
        abort.abort();
        setQueryResult({status: "cancelled"});
    }
}, [invalidate, user]);

Como puede ver, ahora hemos agregado el objeto de usuario a las dependencias del efecto. Si la consulta requiere autenticación, pero el objeto de usuario es nulo, establecemos el resultado de la consulta en "requires_authentication" y regresamos antes, no se realiza ninguna búsqueda. Si pasamos esta verificación, la consulta se activa como de costumbre.

Hacer que el objeto del usuario dependa del efecto de búsqueda también tiene dos buenos efectos secundarios.

Digamos que una consulta requiere que el usuario esté autenticado, pero actualmente no lo está. El resultado de la consulta inicial es "requires_authentication". Si el usuario ahora inicia sesión, el objeto de usuario se actualiza a través del objeto de contexto. Como el objeto de usuario es una dependencia del efecto de búsqueda, todas las consultas ahora se activan nuevamente y el resultado de la consulta se actualiza.

Por otro lado, si una consulta requiere que el usuario esté autenticado y el usuario acaba de cerrar sesión, invalidaremos automáticamente todas las consultas y estableceremos los resultados en "requires_authentication".

¡Excelente! Ahora hemos implementado el patrón de consulta protegido del lado del cliente. Pero ese no es todavía el resultado ideal.

Si está utilizando consultas protegidas del lado del servidor, la navegación del lado del cliente no se maneja correctamente. Por otro lado, si solo usamos consultas protegidas del lado del cliente, siempre volveremos a tener el desagradable parpadeo.

Para resolver estos problemas, tenemos que juntar ambos patrones, lo que nos lleva al patrón de consulta protegido universal.

12. Consulta protegida universal

Este patrón no requiere ningún cambio adicional ya que ya hemos implementado toda la lógica. Todo lo que tenemos que hacer es configurar nuestra página para activar el patrón de consulta protegido universal.

Aquí está el código de la página de consulta protegida universal :

const UniversalProtectedQuery = () => {
    const {user,login,logout} = useWunderGraph();
    const data = useQuery.ProtectedWeather({
        input: {
            city: "Berlin",
        },
    });
    return (
        <div>
            <h1>Universal Protected Query</h1>
            <p>{JSON.stringify(user)}</p>
            <p>{JSON.stringify(data)}</p>
            <button onClick={() => login(AuthProviders.github)}>Login</button>
            <button onClick={() => logout()}>Logout</button>
        </div>
    )
}


export default withWunderGraph(UniversalProtectedQuery);

Juegue con la demostración y vea cómo se comporta cuando inicia y cierra sesión. También intente actualizar la página o use la navegación del lado del cliente.

Lo bueno de este patrón es lo simple que es la implementación real de la página. El gancho de consulta "ProtectedWeather" abstrae toda la complejidad del manejo de la autenticación, tanto del lado del cliente como del lado del servidor.

13. Mutación desprotegida

Correcto, hemos dedicado mucho tiempo a las consultas hasta ahora, ¿qué pasa con las mutaciones? Comencemos con una mutación desprotegida, una que no requiere autenticación. Verá que los enlaces de mutación son mucho más fáciles de implementar que los enlaces de consulta.

function useMutationContextWrapper<Role, Input = never, Data = never>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, mutation: MutationProps): {
    result: MutationResult<Data>;
    mutate: (args?: InternalMutationArgsWithInput<Input>) => Promise<MutationResult<Data>>;
} {
    const {client, user} = useContext(wunderGraphContext);
    const [result, setResult] = useState<MutationResult<Data>>(mutation.requiresAuthentication && user === null ? {status: "requires_authentication"} : {status: "none"});
    const mutate = useCallback(async (args?: InternalMutationArgsWithInput<Input>): Promise<MutationResult<Data>> => {
        setResult({status: "loading"});
        const result = await client.mutate(mutation, args);
        setResult(result as any);
        return result as any;
    }, []);
    return {
        result,
        mutate
    }
}

Las mutaciones no se activan automáticamente. Esto significa que no estamos usando useEffect para desencadenar la mutación. En su lugar, estamos aprovechando el enlace useCallback para crear una función de "mutación" a la que se puede llamar.

Una vez llamado, establecemos el estado del resultado en "cargando" y luego llamamos a la mutación. Cuando finaliza la mutación, establecemos el estado del resultado en el resultado de la mutación. Esto puede ser un éxito o un fracaso. Finalmente, devolvemos tanto el resultado como la función de mutación.

Echa un vistazo a la página de mutaciones sin protección si quieres jugar con este patrón.

Esto fue bastante sencillo. Agreguemos algo de complejidad agregando autenticación.

14. Mutación protegida

function useMutationContextWrapper<Role, Input = never, Data = never>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, mutation: MutationProps): {
    result: MutationResult<Data>;
    mutate: (args?: InternalMutationArgsWithInput<Input>) => Promise<MutationResult<Data>>;
} {
    const {client, user} = useContext(wunderGraphContext);
    const [result, setResult] = useState<MutationResult<Data>>(mutation.requiresAuthentication && user === null ? {status: "requires_authentication"} : {status: "none"});
    const mutate = useCallback(async (args?: InternalMutationArgsWithInput<Input>): Promise<MutationResult<Data>> => {
        if (mutation.requiresAuthentication && user === null) {
            return {status: "requires_authentication"}
        }
        setResult({status: "loading"});
        const result = await client.mutate(mutation, args);
        setResult(result as any);
        return result as any;
    }, [user]);
    useEffect(() => {
        if (!mutation.requiresAuthentication) {
            return
        }
        if (user === null) {
            if (result.status !== "requires_authentication") {
                setResult({status: "requires_authentication"});
            }
            return;
        }
        if (result.status !== "none") {
            setResult({status: "none"});
        }
    }, [user]);
    return {
        result,
        mutate
    }
}

De manera similar al patrón de consulta protegida, estamos inyectando el objeto de usuario del contexto en la devolución de llamada. Si la mutación requiere autenticación, verificamos si el usuario es nulo. Si el usuario es nulo, establecemos el resultado en "requires_authentication" y regresamos antes.

Además, agregamos un efecto para verificar si el usuario es nulo. Si el usuario es nulo, establecemos el resultado en "requires_authentication". Hicimos esto para que las mutaciones cambien automáticamente al estado "requires_authentication" o "ninguno", dependiendo de si el usuario está autenticado o no. De lo contrario, primero tendría que llamar a la mutación para darse cuenta de que no es posible llamar a la mutación. Creo que nos brinda una mejor experiencia de desarrollador cuando está claro por adelantado si la mutación es posible o no.

Muy bien, las mutaciones protegidas ya están implementadas. Quizás se pregunte por qué no hay una sección sobre mutaciones del lado del servidor, protegidas o no. Eso es porque las mutaciones siempre se desencadenan por la interacción del usuario. Por lo tanto, no es necesario que implementemos nada en el servidor.

Dicho esto, queda un problema con las mutaciones, ¡los efectos secundarios! ¿Qué sucede si hay una dependencia entre una lista de tareas y una mutación que cambia las tareas? ¡Hagamos que suceda!

15. Operaciones montadas de recuperación en el éxito de la mutación

Para que esto funcione, necesitamos cambiar tanto la devolución de llamada de mutación como el gancho de consulta. Comencemos con la devolución de llamada de mutación.

const {client, setRefetchMountedOperations, user} = useContext(wunderGraphContext);
const mutate = useCallback(async (args?: InternalMutationArgsWithInput<Input>): Promise<MutationResult<Data>> => {
    if (mutation.requiresAuthentication && user === null) {
        return {status: "requires_authentication"}
    }
    setResult({status: "loading"});
    const result = await client.mutate(mutation, args);
    setResult(result as any);
    if (result.status === "ok" && args?.refetchMountedOperationsOnSuccess === true) {
        setRefetchMountedOperations(prev => prev + 1);
    }
    return result as any;
}, [user]);

Nuestro objetivo es invalidar todas las consultas montadas actualmente cuando una mutación es exitosa. Podemos hacerlo introduciendo otro objeto de estado global que se almacena y propaga a través del contexto React. Llamamos a este objeto de estado "refetchMountedOperationsOnSuccess", que es un contador simple. En caso de que nuestra devolución de llamada de mutación sea exitosa, queremos incrementar el contador. Esto debería ser suficiente para invalidar todas las consultas montadas actualmente.

El segundo paso es cambiar el gancho de consulta.

const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
useEffect(() => {
    if (queryResult?.status === "lazy" || queryResult?.status === "none") {
        return;
    }
    setInvalidate(prev => prev + 1);
}, [refetchMountedOperations]);

Ya debería estar familiarizado con el contador "invalidar". Ahora estamos agregando otro efecto para manejar el incremento del contador "refetchMountedOperations" que se inyectó desde el contexto. Quizás se pregunte por qué volvemos antes si el estado es "perezoso" o "ninguno".

En el caso de "lazy", sabemos que esta consulta aún no se ejecutó, y el desarrollador tiene la intención de ejecutarla solo cuando se active manualmente. Por lo tanto, nos saltamos las consultas perezosas y esperamos hasta que se activen manualmente.

En caso de "ninguno", se aplica la misma regla. Esto podría suceder, por ejemplo, si una consulta solo se procesa en el lado del servidor, pero hemos navegado a la página actual a través de la navegación del lado del cliente. En tal caso, no hay nada que podamos "invalidar", ya que la consulta aún no se ha ejecutado. Tampoco queremos desencadenar por accidente consultas que aún no se ejecutaron a través de un efecto secundario de mutación.

¿Quieres experimentar esto en acción? Dirígete a la página Refetch Mounted Operations on Mutation Success .

¡Frio! Hemos terminado con consultas y mutaciones. A continuación, veremos la implementación de ganchos para suscripciones.

16. Suscripción del lado del cliente

Para implementar suscripciones, tenemos que crear un nuevo gancho dedicado:

function useSubscriptionContextWrapper<Input, Data, Role>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, subscription: SubscriptionProps, args?: InternalSubscriptionArgsWithInput<Input>): {
    result: SubscriptionResult<Data>;
} {
    const {ssrCache, client} = useContext(wunderGraphContext);
    const cacheKey = client.cacheKey(subscription, args);
    const [invalidate, setInvalidate] = useState<number>(0);
    const [subscriptionResult, setSubscriptionResult] = useState<SubscriptionResult<Data> | undefined>(ssrCache[cacheKey] as SubscriptionResult<Data> || {status: "none"});
    useEffect(() => {
        if (subscriptionResult?.status === "ok") {
            setSubscriptionResult({...subscriptionResult, streamState: "restarting"});
        } else {
            setSubscriptionResult({status: "loading"});
        }
        const abort = new AbortController();
        client.subscribe(subscription, (response: SubscriptionResult<Data>) => {
            setSubscriptionResult(response as any);
        }, {
            ...args,
            abortSignal: abort.signal
        });
        return () => {
            abort.abort();
        }
    }, [invalidate]);
    return {
        result: subscriptionResult as SubscriptionResult<Data>
    }
}

La implementación de este enlace es similar al enlace de consulta. Se activa automáticamente cuando se monta el componente envolvente, por lo que estamos usando el gancho "useEffect" nuevamente.

Es importante pasar una señal de cancelación al cliente para asegurarse de que la suscripción se cancela cuando se desmonta el componente. Además, queremos cancelar y reiniciar la suscripción cuando se incremente el contador de invalidaciones, similar al gancho de consulta.

Hemos omitido la autenticación por brevedad en este punto, pero puede suponer que es muy similar al gancho de consulta.

¿Quieres jugar con el ejemplo? Dirígete a la página de suscripción del lado del cliente .

Sin embargo, una cosa a tener en cuenta es que las suscripciones se comportan de manera diferente a las consultas. Las suscripciones son un flujo de datos que se actualiza continuamente. Esto significa que tenemos que pensar cuánto tiempo queremos mantener abierta la suscripción. ¿Debe permanecer abierto para siempre? ¿O podría darse el caso de que queramos parar y reanudar la suscripción?

Uno de esos casos es cuando el usuario desenfoca la ventana, lo que significa que ya no está usando activamente la aplicación.

17. Detener la suscripción en Window Blur

Para detener la suscripción cuando el usuario desenfoca la ventana, debemos extender el gancho de suscripción:

function useSubscriptionContextWrapper<Input, Data, Role>(wunderGraphContext: Context<WunderGraphContextProperties<Role>>, subscription: SubscriptionProps, args?: InternalSubscriptionArgsWithInput<Input>): {
    result: SubscriptionResult<Data>;
} {
    const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
    const isServer = typeof window === 'undefined';
    const ssrEnabled = args?.disableSSR !== true;
    const cacheKey = client.cacheKey(subscription, args);
    const [stop, setStop] = useState(false);
    const [invalidate, setInvalidate] = useState<number>(0);
    const [stopOnWindowBlur] = useState(args?.stopOnWindowBlur === true);
    const [subscriptionResult, setSubscriptionResult] = useState<SubscriptionResult<Data> | undefined>(ssrCache[cacheKey] as SubscriptionResult<Data> || {status: "none"});
    useEffect(() => {
        if (stop) {
            if (subscriptionResult?.status === "ok") {
                setSubscriptionResult({...subscriptionResult, streamState: "stopped"});
            } else {
                setSubscriptionResult({status: "none"});
            }
            return;
        }
        if (subscriptionResult?.status === "ok") {
            setSubscriptionResult({...subscriptionResult, streamState: "restarting"});
        } else {
            setSubscriptionResult({status: "loading"});
        }
        const abort = new AbortController();
        client.subscribe(subscription, (response: SubscriptionResult<Data>) => {
            setSubscriptionResult(response as any);
        }, {
            ...args,
            abortSignal: abort.signal
        });
        return () => {
            abort.abort();
        }
    }, [stop, refetchMountedOperations, invalidate, user]);
    useEffect(() => {
        if (!stopOnWindowBlur) {
            return
        }
        if (isWindowFocused === "focused") {
            setStop(false);
        }
        if (isWindowFocused === "blurred") {
            setStop(true);
        }
    }, [stopOnWindowBlur, isWindowFocused]);
    return {
        result: subscriptionResult as SubscriptionResult<Data>
    }
}

Para que esto funcione, introducimos una nueva variable con estado llamada "stop". El estado predeterminado será falso, pero cuando el usuario desenfoca la ventana, estableceremos el estado en verdadero. Si vuelven a entrar en la ventana (foco), estableceremos el estado de nuevo en falso. Si el desarrollador establece "stopOnWindowBlur" en falso, lo ignoraremos, lo que se puede configurar en el objeto "args" de las suscripciones.

Además, tenemos que agregar la variable de parada a las dependencias de suscripción. ¡Eso es todo! Es muy útil que hayamos manejado los eventos de la ventana globalmente, esto hace que todos los demás ganchos sean mucho más fáciles de implementar.

La mejor manera de experimentar la implementación es abrir la página de Suscripción del lado del cliente y mirar atentamente la pestaña de red en la consola de Chrome DevTools (o similar si está usando otro navegador).

Volviendo a uno de los problemas que hemos descrito inicialmente, todavía tenemos que dar una respuesta a la pregunta de cómo podemos implementar la representación del lado del servidor para las suscripciones, haciendo que el gancho de suscripciones sea "universal".

18. Suscripción Universal

Quizás esté pensando que la representación del lado del servidor no es posible para las suscripciones. Quiero decir, ¿cómo debe renderizar el servidor un flujo de datos?

Si es un lector habitual de este blog, es posible que conozca nuestra Implementación de suscripción. Como describimos en otro blog , implementamos las suscripciones de GraphQL de una manera que es compatible con EventSource (SSE), así como con la API Fetch.

También hemos agregado una bandera especial a la implementación. El cliente puede establecer el parámetro de consulta "wg_subscribe_once" en verdadero. Lo que esto significa es que una suscripción, con este indicador establecido, es esencialmente una consulta.

Aquí está la implementación del cliente para obtener una consulta:

const params = this.queryString({
    wg_variables: args?.input,
    wg_api_hash: this.applicationHash,
    wg_subscribe_once: args?.subscribeOnce,
});
const headers: Headers = {
    ...this.extraHeaders,
    Accept: "application/json",
    "WG-SDK-Version": this.sdkVersion,
};
const defaultOrCustomFetch = this.customFetch || globalThis.fetch;
const url = this.baseURL + "/" + this.applicationPath + "/operations/" + query.operationName + params;
const response = await defaultOrCustomFetch(url,
    {
        headers,
        method: 'GET',
        credentials: "include",
        mode: "cors",
    }
);

Tomamos las variables, un hash de la configuración y el indicador subscribeOnce y los codificamos en la cadena de consulta. Si se establece suscribirse una vez, está claro para el servidor que solo queremos el primer resultado de la suscripción.

Para brindarle una imagen completa, veamos también la implementación de las suscripciones del lado del cliente:

private subscribeWithSSE = <S extends SubscriptionProps, Input, Data>(subscription: S, cb: (response: SubscriptionResult<Data>) => void, args?: InternalSubscriptionArgs) => {
    (async () => {
        try {
            const params = this.queryString({
                wg_variables: args?.input,
                wg_live: subscription.isLiveQuery ? true : undefined,
                wg_sse: true,
                wg_sdk_version: this.sdkVersion,
            });
            const url = this.baseURL + "/" + this.applicationPath + "/operations/" + subscription.operationName + params;
            const eventSource = new EventSource(url, {
                withCredentials: true,
            });
            eventSource.addEventListener('message', ev => {
                const responseJSON = JSON.parse(ev.data);
                // omitted for brevity
                if (responseJSON.data) {
                    cb({
                        status: "ok",
                        streamState: "streaming",
                        data: responseJSON.data,
                    });
                }
            });
            if (args?.abortSignal) {
                args.abortSignal.addEventListener("abort", () => eventSource.close());
            }
        } catch (e: any) {
            // omitted for brevity
        }
    })();
};

La implementación del cliente de suscripción es similar al cliente de consulta, excepto que usamos la API de EventSource con una devolución de llamada. Si EventSource no está disponible, recurrimos a Fetch API, pero mantendré la implementación fuera de la publicación del blog, ya que no agrega mucho valor adicional.

Lo único importante que debe quitar de esto es que agregamos un oyente a la señal de cancelación. Si el componente adjunto se desmonta o invalida, activará el evento de cancelación, que cerrará EventSource.

Tenga en cuenta que si estamos haciendo un trabajo asíncrono de cualquier tipo, siempre debemos asegurarnos de que manejamos la cancelación correctamente, de lo contrario, podríamos terminar con una pérdida de memoria.

Bien, ahora conoce la implementación del cliente de suscripción. Envolvamos al cliente con ganchos de suscripción fáciles de usar que se pueden usar tanto en el cliente como en el servidor.

const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
const isServer = typeof window === 'undefined';
const ssrEnabled = args?.disableSSR !== true;
const cacheKey = client.cacheKey(subscription, args);
if (isServer) {
    if (ssrEnabled) {
        if (ssrCache[cacheKey]) {
            return {
                result: ssrCache[cacheKey] as SubscriptionResult<Data>
            }
        }
        const promise = client.query(subscription, {...args, subscribeOnce: true});
        ssrCache[cacheKey] = promise;
        throw promise;
    } else {
        ssrCache[cacheKey] = {
            status: "none",
        }
        return {
            result: ssrCache[cacheKey] as SubscriptionResult<Data>
        }
    }
}

De manera similar al enlace useQuery, agregamos una rama de código para la representación del lado del servidor. Si estamos en el servidor y aún no tenemos ningún dato, hacemos una solicitud de "consulta" con el indicador subscribeOnce establecido en verdadero. Como se describió anteriormente, una suscripción con el indicador subscribeOnce establecido en verdadero, solo devolverá el primer resultado, por lo que se comporta como una consulta. Es por eso que usamos client.query()en lugar de client.subscribe().

Algunos comentarios en la publicación del blog sobre nuestra implementación de suscripción indicaron que no es tan importante hacer que las suscripciones sean sin estado. Espero que en este punto quede claro por qué hemos ido por este camino. La compatibilidad con Fetch acaba de aterrizar en NodeJS, e incluso antes de eso, hemos tenido node-fetch como un polyfill. Definitivamente sería posible iniciar suscripciones en el servidor usando WebSockets, pero en última instancia, creo que es mucho más fácil simplemente usar la API Fetch y no tener que preocuparse por las conexiones de WebSocket en el servidor.

La mejor manera de jugar con esta implementación es ir a la página de suscripción universal . Cuando actualice la página, eche un vistazo a la "vista previa" de la primera solicitud. Verá que la página vendrá renderizada por el servidor en comparación con la suscripción del lado del cliente. Una vez que el cliente se rehidrate, iniciará una suscripción por sí mismo para mantener actualizada la interfaz de usuario.

Eso fue mucho trabajo, pero aún no hemos terminado. Las suscripciones también deben protegerse mediante la autenticación, agreguemos algo de lógica al gancho de suscripción.

19. Suscripción protegida

Notarás que es muy similar a un gancho de consulta normal.

const {ssrCache, client, isWindowFocused, refetchMountedOperations, user} = useContext(wunderGraphContext);
const [subscriptionResult, setSubscriptionResult] = useState<SubscriptionResult<Data> | undefined>(ssrCache[cacheKey] as SubscriptionResult<Data> || {status: "none"});
useEffect(() => {
    if (subscription.requiresAuthentication && user === null) {
        setSubscriptionResult({
            status: "requires_authentication",
        });
        return;
    }
    if (stop) {
        if (subscriptionResult?.status === "ok") {
            setSubscriptionResult({...subscriptionResult, streamState: "stopped"});
        } else {
            setSubscriptionResult({status: "none"});
        }
        return;
    }
    if (subscriptionResult?.status === "ok") {
        setSubscriptionResult({...subscriptionResult, streamState: "restarting"});
    } else {
        setSubscriptionResult({status: "loading"});
    }
    const abort = new AbortController();
    client.subscribe(subscription, (response: SubscriptionResult<Data>) => {
        setSubscriptionResult(response as any);
    }, {
        ...args,
        abortSignal: abort.signal
    });
    return () => {
        abort.abort();
    }
}, [stop, refetchMountedOperations, invalidate, user]);

Primero, tenemos que agregar el usuario como una dependencia para el efecto. Esto hará que el efecto se dispare cada vez que cambie el usuario. Luego, debemos verificar los metadatos de la suscripción y ver si requiere autenticación. Si lo hace, comprobamos si el usuario está logueado. Si el usuario está logueado, continuamos con la suscripción. Si el usuario no ha iniciado sesión, establecemos el resultado de la suscripción en "requires_authentication".

¡Eso es todo! ¡Suscripciones universales con reconocimiento de autenticación completadas! Echemos un vistazo a nuestro resultado final:

const ProtectedSubscription = () => {
    const {login,logout,user} = useWunderGraph();
    const data = useSubscription.ProtectedPriceUpdates();
    return (
        <div>
            <p>{JSON.stringify(user)}</p>
            <p style={{height: "8vh"}}>{JSON.stringify(data)}</p>
            <button onClick={() => login(AuthProviders.github)}>Login</button>
            <button onClick={() => logout()}>Logout</button>
        </div>
    )
}


export default withWunderGraph(ProtectedSubscription);

¿No es genial cómo podemos ocultar tanta complejidad detrás de una API simple? Todas estas cosas, como la autenticación, el enfoque y el desenfoque de la ventana, la representación del lado del servidor, la representación del lado del cliente, el paso de datos del servidor al cliente, la rehidratación adecuada del cliente, todo lo manejamos nosotros.

Además de eso, el cliente usa principalmente genéricos y está envuelto por una pequeña capa de código generado, lo que hace que todo el cliente sea completamente seguro. La seguridad tipográfica era uno de nuestros requisitos, si recuerdas.

Algunos clientes de API "pueden" tener seguridad de tipos. Otros le permiten agregar algún código adicional para que sean de tipo seguro. Con nuestro enfoque, un cliente genérico más tipos generados automáticamente, el cliente siempre tiene seguridad de tipos.

Es un manifiesto para nosotros que, hasta ahora, nadie nos ha pedido que agreguemos un cliente de JavaScript "puro". Nuestros usuarios parecen aceptar y apreciar que todo es seguro desde el primer momento. Creemos que la seguridad de tipos ayuda a los desarrolladores a cometer menos errores y a comprender mejor su código.

¿Quieres jugar tú mismo con suscripciones universales protegidas? Consulte la página de suscripción protegida de la demostración. No olvide consultar Chrome DevTools y la pestaña de red para obtener la mejor información.

Finalmente, hemos terminado con las suscripciones. Faltan dos patrones más y hemos terminado por completo.

20. Consulta en vivo del lado del cliente

El último patrón que vamos a cubrir es Live Queries. Las consultas en vivo son similares a las suscripciones en la forma en que se comportan en el lado del cliente. Donde difieren es en el lado del servidor.

Primero analicemos cómo funcionan las consultas en vivo en el servidor y por qué son útiles. Si un cliente se "suscribe" a una consulta en vivo, el servidor comenzará a sondear el servidor de origen en busca de cambios. Lo hará en un intervalo configurable, por ejemplo, cada segundo. Cuando el servidor recibe un cambio, analizará los datos y los comparará con el hash del último cambio. Si los hashes son diferentes, el servidor enviará los nuevos datos al cliente. Si los hashes son los mismos, sabemos que nada cambió, por lo que no enviamos nada al cliente.

¿Por qué y cuándo son útiles las consultas en vivo? En primer lugar, gran parte de la infraestructura existente no admite suscripciones. Agregar consultas en vivo en el nivel de la puerta de enlace significa que puede agregar capacidades de "tiempo real" a su infraestructura existente. Podría tener un backend de PHP heredado que ya no quiera tocar. Agregue consultas en vivo encima y su interfaz podrá recibir actualizaciones en tiempo real.

Quizás se esté preguntando por qué no simplemente hacer el sondeo desde el lado del cliente. El sondeo del lado del cliente podría generar muchas solicitudes al servidor. Imagínese si 10.000 clientes hacen una solicitud por segundo. Eso es 10.000 solicitudes por segundo. ¿Crees que tu servidor PHP heredado puede manejar ese tipo de carga?

¿Cómo pueden ayudar las consultas en vivo? 10.000 clientes se conectan a la puerta de enlace api y se suscriben a una consulta en vivo. Luego, la puerta de enlace puede agrupar todas las solicitudes, ya que esencialmente solicitan los mismos datos y realizar una sola solicitud al origen.

Al usar consultas en vivo, podemos reducir la cantidad de solicitudes al servidor de origen, según la cantidad de "flujos" que se usen.

Entonces, ¿cómo podemos implementar consultas en vivo en el cliente?

Eche un vistazo al envoltorio "generado" del cliente genérico para una de nuestras operaciones:

CountryWeather: (args: SubscriptionArgsWithInput<CountryWeatherInput>) =>
    hooks.useSubscriptionWithInput<CountryWeatherInput, CountryWeatherResponseData, Role>(WunderGraphContext, {
        operationName: "CountryWeather",
        isLiveQuery: true,
        requiresAuthentication: false,
    })(args)

Mirando este ejemplo, puede notar algunas cosas. Primero, estamos usando el useSubscriptionWithInputgancho. Esto indica que en realidad no tenemos que distinguir entre una suscripción y una consulta en vivo, al menos no desde la perspectiva del lado del cliente. La única diferencia es que estamos configurando la isLiveQuerybandera en true. Para las suscripciones, usamos el mismo enlace, pero establecemos la isLiveQuerymarca en false.

Como ya implementamos el enlace de suscripción anterior, no se requiere código adicional para que las consultas en vivo funcionen.

Consulte la página de consulta en vivo de la demostración. Una cosa que puede notar es que este ejemplo tiene el desagradable parpadeo nuevamente, eso se debe a que no lo estamos renderizando del lado del servidor.

21. Consulta en vivo universal

El patrón final y último que vamos a cubrir es Universal Live Queries. Las consultas en vivo universales son similares a las suscripciones, solo que más simples desde la perspectiva del lado del servidor. Para que el servidor inicie una suscripción, tiene que abrir una conexión WebSocket con el servidor de origen, hacer el protocolo de enlace, suscribirse, etc. Si necesitamos suscribirnos una vez con una consulta en vivo, simplemente estamos "sondeando" una vez , lo que significa que solo estamos haciendo una sola solicitud. Por lo tanto, las consultas en vivo son en realidad un poco más rápidas de iniciar en comparación con las suscripciones, al menos en la solicitud inicial.

¿Cómo podemos usarlos? Veamos un ejemplo de la demostración:

const UniversalLiveQuery = () => {
    const data = useLiveQuery.CountryWeather({
        input: {
            code: "DE",
        },
    });
    return (
        <p>{JSON.stringify(data)}</p>
    )
}


export default withWunderGraph(UniversalLiveQuery);

Eso es todo, ese es su flujo de datos meteorológicos para la capital de Alemania, Berlín, que se actualiza cada segundo.

Quizás se pregunte cómo obtuvimos los datos en primer lugar. Veamos la definición de la CountryWeatheroperación:

query ($capital: String! @internal $code: ID!) {
    countries_country(code: $code){
        code
        name
        capital @export(as: "capital")
        weather: _join  @transform(get: "weather_getCityByName.weather") {
            weather_getCityByName(name: $capital){
                weather {
                    temperature {
                        actual
                    }
                    summary {
                        title
                        description
                    }
                }
            }
        }
    }
}

En realidad estamos uniendo datos de dos servicios dispares. Primero, estamos usando una API de países para obtener la capital de un país. Exportamos el campo a la variable capitalinterna . $capitalLuego, usamos el _joincampo para combinar los datos del país con una API meteorológica. Finalmente, aplicamos la @transformdirectiva para aplanar un poco la respuesta.

Es una consulta GraphQL normal y válida. En combinación con el patrón de consulta en vivo, ahora podemos transmitir en vivo el clima de cualquier capital de cualquier país. Genial, ¿no?

Al igual que todos los demás patrones, este también se puede probar en la demostración. ¡ Dirígete a la página de consulta universal en vivo y juega!

¡Eso es todo! ¡Hemos terminado! Espero que haya aprendido cómo puede crear ganchos de obtención de datos universales y conscientes de la autenticación.

Antes de que lleguemos al final de esta publicación, me gustaría ver enfoques y herramientas alternativos para implementar ganchos de obtención de datos.

Enfoques alternativos para la obtención de datos en NextJS

SSG (Generación de sitios estáticos)

Una de las principales desventajas de utilizar la representación del lado del servidor es que el cliente tiene que esperar hasta que el servidor haya terminado de representar la página. Dependiendo de la complejidad de la página, esto puede llevar un tiempo, especialmente si tiene que realizar muchas solicitudes encadenadas para obtener todos los datos necesarios para la página.

Una solución a este problema es generar estáticamente la página en el servidor. NextJS le permite implementar una getStaticPropsfunción asíncrona en la parte superior de cada página. Esta función se llama en el momento de la creación y es responsable de obtener todos los datos necesarios para la página. Si, al mismo tiempo, no adjunta una función getInitialPropso getServerSidePropsa la página, NextJS considera que esta página es estática, lo que significa que no se requerirá ningún proceso de NodeJS para representar la página. En este escenario, la página se renderizará previamente en el momento de la compilación, lo que permitirá que una CDN la almacene en caché.

Esta forma de renderizar hace que la aplicación sea extremadamente rápida y fácil de alojar, pero también tiene inconvenientes.

Por un lado, una página estática no es específica del usuario. Eso es porque en tiempo de construcción, no hay contexto del usuario. Sin embargo, esto no es un problema para las páginas públicas. Es solo que no puede usar páginas específicas de usuario como tableros de esta manera.

Una compensación que se puede hacer es renderizar la página de forma estática y agregar contenido específico del usuario en el lado del cliente. Sin embargo, esto siempre generará parpadeo en el cliente, ya que la página se actualizará muy poco tiempo después del renderizado inicial. Por lo tanto, si está creando una aplicación que requiere que el usuario esté autenticado, es posible que desee utilizar la representación del lado del servidor en su lugar.

El segundo inconveniente de la generación de sitios estáticos es que el contenido puede quedar obsoleto si los datos subyacentes cambian. En ese caso, es posible que desee reconstruir la página. Sin embargo, la reconstrucción de toda la página puede llevar mucho tiempo y puede ser innecesaria si solo se necesita reconstruir unas pocas páginas. Afortunadamente, hay una solución a este problema: la regeneración estática incremental.

ISR (regeneración estática incremental)

La regeneración estática incremental le permite invalidar páginas individuales y volver a procesarlas a pedido. Esto le brinda la ventaja de rendimiento de un sitio estático, pero elimina el problema del contenido obsoleto.

Dicho esto, esto todavía no resuelve el problema con la autenticación, pero no creo que esto sea de lo que se trata la generación de sitios estáticos.

Por nuestra parte, actualmente estamos analizando patrones en los que el resultado de una mutación podría desencadenar automáticamente una reconstrucción de página mediante ISR. Idealmente, esto podría ser algo que funcione de forma declarativa, sin tener que implementar una lógica personalizada.

Fragmentos de GraphQL

Un problema con el que se puede encontrar con la representación del lado del servidor (pero también del lado del cliente) es que al atravesar el árbol de componentes, el servidor puede tener que crear una enorme cascada de consultas que dependen unas de otras. Si los componentes secundarios dependen de los datos de sus padres, es posible que se encuentre fácilmente con el problema N+1.

N+1 en este caso significa que obtiene una matriz de datos en un componente raíz y luego, para cada uno de los elementos de la matriz, tendrá que activar una consulta adicional en un componente secundario.

Tenga en cuenta que este problema no es específico del uso de GraphQL. GraphQL en realidad tiene una solución para resolverlo, mientras que las API REST sufren el mismo problema. La solución es usar fragmentos de GraphQL con un cliente que los admita adecuadamente.

Los creadores de GraphQL, Facebook/Meta, han creado una solución para este problema, se llama Relay Client.

Relay Client es una biblioteca que le permite especificar sus "Requisitos de datos" junto con los componentes a través de fragmentos de GraphQL. Aquí hay un ejemplo de cómo podría verse esto:

import type {UserComponent_user$key} from 'UserComponent_user.graphql';


const React = require('React');


const {graphql, useFragment} = require('react-relay');


type Props = {
  user: UserComponent_user$key,
};


function UserComponent(props: Props) {
  const data = useFragment(
    graphql`
      fragment UserComponent_user on User {
        name
        profile_picture(scale: 2) {
          uri
        }
      }
    `,
    props.user,
  );


  return (
    <>
      <h1>{data.name}</h1>
      <div>
        <img src={data.profile_picture?.uri} />
      </div>
    </>
  );
}

Si este fuera un componente anidado, el fragmento nos permite elevar nuestros requisitos de datos hasta el componente raíz. Esto significa que el componente raíz será capaz de obtener los datos de sus elementos secundarios, manteniendo la definición de requisitos de datos en los componentes secundarios.

Los fragmentos permiten un acoplamiento flexible entre los componentes principales y secundarios, al tiempo que permiten un proceso de obtención de datos más eficiente. Para muchos desarrolladores, esta es la razón real por la que usan GraphQL. No es que usen GraphQL porque quieran usar el lenguaje de consulta, es porque quieren aprovechar el poder del cliente de retransmisión.

Para nosotros, el Cliente de Relay es una gran fuente de inspiración. De hecho, creo que usar Relay es demasiado difícil. En nuestra próxima iteración, buscamos adoptar el enfoque de "elevación de fragmentos", pero nuestro objetivo es que sea más fácil de usar que el cliente de retransmisión.

reaccionar suspenso

Otro desarrollo que está ocurriendo en el mundo de React es la creación de React Suspense. Como ha visto anteriormente, ya estamos usando Suspense en el servidor. Al "lanzar" una promesa, podemos suspender la representación de un componente hasta que se resuelva la promesa. Esa es una excelente manera de manejar la obtención de datos asincrónicos en el servidor.

Sin embargo, también puede aplicar esta técnica en el cliente. El uso de Suspense en el cliente nos permite "renderizar mientras se obtiene" de una manera muy eficiente. Además, los clientes que admiten Suspense permiten una API más elegante para enlaces de obtención de datos. En lugar de tener que manejar estados de "carga" o "error" dentro del componente, el suspenso "empujará" estos estados al siguiente "límite de error" y los manejará allí. Este enfoque hace que el código dentro del componente sea mucho más legible, ya que solo maneja el "camino feliz".

Como ya admitimos Suspense en el servidor, puede estar seguro de que también agregaremos soporte al cliente en el futuro. Solo queremos descubrir la forma más idiomática de apoyar tanto a un cliente de suspenso como a uno que no lo es. De esta manera, los usuarios obtienen la libertad de elegir el estilo de programación que prefieran.

Tecnologías alternativas para la obtención y autenticación de datos en NextJS

No somos los únicos que intentamos mejorar la experiencia de obtención de datos en NextJS. Por lo tanto, echemos un vistazo rápido a otras tecnologías y cómo se comparan con el enfoque que estamos proponiendo.

swr

De hecho, nos hemos inspirado mucho en swr. Si observa los patrones que hemos implementado, verá que swr realmente nos ayudó a definir una excelente API de obtención de datos.

Hay algunas cosas en las que nuestro enfoque difiere del swr que vale la pena mencionar.

SWR es mucho más flexible y fácil de adoptar porque puede usarlo con cualquier backend. El enfoque que hemos adoptado, especialmente la forma en que manejamos la autenticación, requiere que también ejecute un backend de WunderGraph que proporcione la API que esperamos.

Por ejemplo, si está utilizando el cliente WunderGraph, esperamos que el backend sea una parte dependiente de OpenID Connect. El cliente swr, por otro lado, no hace tales suposiciones.

Personalmente, creo que con una biblioteca como swr, eventualmente obtendrá un resultado similar al que obtendría si estuviera usando el cliente WunderGraph en primer lugar. Es solo que ahora está manteniendo más código ya que tuvo que agregar lógica de autenticación.

La otra gran diferencia es la representación del lado del servidor. WunderGraph está cuidadosamente diseñado para eliminar cualquier parpadeo innecesario al cargar una aplicación que requiere autenticación. Los documentos de swr explican que esto no es un problema y que los usuarios están de acuerdo con cargar spinners en los tableros.

Creo que podemos hacerlo mejor que eso. Sé de paneles SaaS que tardan 15 segundos o más en cargar todos los componentes, incluido el contenido. Durante este período de tiempo, la interfaz de usuario no se puede usar en absoluto, porque sigue "moviendo" todo el contenido en el lugar correcto.

¿Por qué no podemos renderizar previamente todo el tablero y luego rehidratar al cliente? Si el HTML se representa de la manera correcta, se debe poder hacer clic en los enlaces incluso antes de que se cargue el cliente de JavaScript.

Si todo su "backend" cabe en el directorio "/api" de su aplicación NextJS, su mejor opción probablemente sea usar la biblioteca "swr". Combinado con NextAuthJS, esto puede ser una muy buena combinación.

Si, en cambio, está creando servicios dedicados para implementar API, un enfoque de "backend para frontend", como el que proponemos con WunderGraph, podría ser una mejor opción, ya que podemos eliminar muchos cierres de sesión repetitivos. de sus servicios y en el middleware.

SiguienteAuthJS

Hablando de NextAuthJS, ¿por qué no simplemente agregar la autenticación directamente en su aplicación NextJS? La biblioteca está diseñada para resolver exactamente este problema, agregando autenticación a su aplicación NextJS con un mínimo esfuerzo.

Desde una perspectiva técnica, NextAuthJS sigue patrones similares a WunderGraph. Solo hay algunas diferencias en términos de la arquitectura general.

Si está creando una aplicación que nunca escalará más allá de un solo sitio web, probablemente pueda usar NextAuthJS. Sin embargo, si planea usar varios sitios web, herramientas cli, aplicaciones nativas o incluso conectar un backend, es mejor que use un enfoque diferente.

Déjame explicarte por qué.

La forma en que se implementa NextAuthJS es que en realidad se convierte en el "Emisor" del flujo de autenticación. Dicho esto, no es un emisor compatible con OpenID Connect, es una implementación personalizada. Entonces, si bien es fácil comenzar, en realidad está agregando una gran cantidad de deuda técnica al principio.

Supongamos que le gustaría agregar otro tablero o una herramienta cli o conectar un backend a sus API. Si estaba usando un emisor compatible con OpenID Connect, ya hay un flujo implementado para varios escenarios diferentes. Además, este proveedor de OpenID Connect solo está ligeramente acoplado a su aplicación NextJS. Hacer que su propia aplicación sea el emisor significa que debe volver a implementar y modificar su aplicación "frontend", cada vez que desee modificar el flujo de autenticación. Tampoco podrá usar flujos de autenticación estandarizados como el flujo de código con pkce o el flujo del dispositivo.

La autenticación debe gestionarse fuera de la propia aplicación. Recientemente anunciamos nuestra asociación con Cloud IAM , lo que hace que la configuración de un proveedor de OpenID Connect con WunderGraph como la parte de confianza sea cuestión de minutos.

Espero que se lo pongamos lo suficientemente fácil para que no tenga que crear sus propios flujos de autenticación.

trpc

La capa de obtención de datos y los ganchos son en realidad muy parecidos a WunderGraph. Creo que incluso estamos usando el mismo enfoque para la representación del lado del servidor en NextJS.

El trpc obviamente tiene muy poco que ver con GraphQL, en comparación con WunderGraph. Su historia sobre la autenticación tampoco es tan completa como WunderGraph.

Dicho esto, creo que Alex ha hecho un gran trabajo al construir trpc. Es menos obstinado que WunderGraph, lo que lo convierte en una excelente opción para diferentes escenarios.

Según tengo entendido, trpc funciona mejor cuando tanto el backend como el frontend usan TypeScript. WunderGraph toma un camino diferente. El término medio común para definir el contrato entre el cliente y el servidor es JSON-RPC, definido mediante JSON Schema. En lugar de simplemente importar los tipos de servidor al cliente, debe pasar por un proceso de generación de código con WunderGraph.

Esto significa que la configuración es un poco más compleja, pero no solo podemos admitir TypeScript como entorno de destino, sino cualquier otro lenguaje o tiempo de ejecución que admita JSON a través de HTTP.

Otros clientes de GraphQL

Hay muchos otros clientes de GraphQL, como Apollo Client, urql y graphql-request. Lo que todos ellos tienen en común es que no suelen utilizar JSON-RPC como transporte.

Probablemente haya escrito esto en varias publicaciones de blog antes, pero enviar solicitudes de lectura a través de HTTP POST simplemente rompe Internet. Si no está cambiando las operaciones GraphQL, como el 99 % de todas las aplicaciones que usan un paso de compilación/transpilación, ¿por qué usar un cliente GraphQL que hace esto?

Clientes, navegadores, servidores de caché, servidores proxy y CDN, todos entienden los encabezados de control de caché y las etiquetas electrónicas. El popular cliente de obtención de datos NextJS "swr" tiene su nombre por una razón, porque swr significa "obsoleto mientras se revalida", que no es más que el patrón que aprovecha ETags para una invalidación de caché eficiente.

GraphQL es una gran abstracción para definir dependencias de datos. Pero cuando se trata de implementar aplicaciones a escala web, debemos aprovechar la infraestructura existente de la web. Lo que esto significa es esto: GraphQL es excelente durante el desarrollo, pero en la producción, deberíamos aprovechar los principios de REST tanto como podamos.

Resumen

Crear buenos ganchos de obtención de datos para NextJS y React en general es un desafío. También hemos discutido que estamos llegando a soluciones algo diferentes si tomamos en cuenta la autenticación desde el principio. Personalmente, creo que agregar autenticación directamente en la capa API en ambos extremos, backend y frontend, hace que el enfoque sea mucho más limpio. Otro aspecto a tener en cuenta es dónde colocar la lógica de autenticación. Idealmente, no lo está implementando usted mismo, pero puede confiar en una implementación adecuada. Combinar OpenID Connect como emisor con una parte dependiente en su back-end-for-frontend (BFF) es una excelente manera de mantener las cosas desacopladas pero aún muy controlables.

Nuestro BFF todavía está creando y validando cookies, pero no es la fuente de la verdad. Siempre estamos delegando a Keycloak. Lo bueno de esta configuración es que puede cambiar fácilmente Keycloak por otra implementación, esa es la belleza de confiar en interfaces en lugar de implementaciones concretas.

Finalmente, espero poder convencerlo de que más tableros (SaaS) deberían adoptar la representación del lado del servidor. NextJS y WunderGraph hacen que sea tan fácil de implementar que vale la pena intentarlo.

Una vez más, si está interesado en jugar con una demostración, aquí está el repositorio: https://github.com/wundergraph/wundergraph-demo

Fuente: https://wundergraph.com/blog/nextjs_and_react_ssr_21_universal_data_fetching_patterns_and_best_practices

#saas #react #nextjs #restapi 

NextJS / React SSR: 21 Patrones Universales De Obtención De Datos
Delbert  Ferry

Delbert Ferry

1651892400

Blendbase: Single Open-source GraphQL API to Connect CRMs To Your SaaS

Blendbase provides single unified GraphQL API to access CRMs for SaaS solutions builders. Instead of building and maintaining various integrations with third-party apps (e.g. Salesforce, HubSpot CRM, PipeDrive, etc.), you can use Blendbase CRM API to access each of these through a single unified interface, no matter what CRM your users use. Blendbase also manages the complexity of authentication and authorization with CRMs.

Check out more at blendbase.io or join our Discord.

Supported CRMs

  • Salesforce
  • HubSpot

Configuring Blendbase

  1. Copy .env.sample cp .env.sample .env
  2. Copy .env.sample connect-fullstack-webapp-sample/.env.sample connect-fullstack-webapp-sample/.env
  3. Generate a secret used for encrypting sensitive information and store it in SECRET_ENCRYPTION_KEY in .env: go run main.go gen-enc-key
  4. Blendbase is using JWT tokens to authenticate both Omni and Connect APIs (both client side and server-side). Generate a secret that is going to be used for encrypting JWT tokens by running openssl rand -hex 32 and store it in
  • BLENDBASE_AUTH_SECRET in ~/.env.local
  • BLENDBASE_AUTH_SECRET in ~/connect-fullstack-webapp-sample/.env
  1. docker-compose build
  2. docker-compose up
  3. Go to http://localhost:3000/ to see the sample Connect app, follow the instructions below on configuring CRM integrations
  4. Run go run main.go gen-auth-token --consumer-id c6a82fd9-7e22-40c2-8bf2-db58a40839a9 to obtain an authentication token (the used consumer ID is preconfigured for test purposes)
  5. Go to http://localhost:8080/ and configure HTTP headers (replace $token with the value from the previous step)
{
  "Authorization": "Bearer $token"
}
  1. Test Omni API with the query
query {
  crm {
    contacts {
      edges {
        node {
          id
          name
        }
      }
    }
  }
}

Connecting to Salesforce

  1. Login to your instances of Salesforce or create a new one at https://login.salesforce.com/ as an admin
  2. Go to Settings > Setup > Apps > App Manager
  3. Click on "New Connected App"
    1. Fill out the name and contact email
    2. Enable "Enable OAuth Settings"
    3. Fill out "Callback URL" with https://example.com - we will change it later
    4. In the "Selected OAuth Scopes" select:
      1. Manage user data via APIs (api)
      2. Perform Requests at any time (…)
    5. Click "Save"
  4. Set "Consumer Key" and "Consumer Secret" to h at http://localhost:3000/ (SALESFORCE_CLIENT_ID and SALESFORCE_CLIENT_SECRET in .env for development and testing).

Connecting to HubSpot

  1. Log in to or create a new instance of HubSpot at https://app.hubspot.com/login as admin
  2. Go to Settings (cog icon in the top-left)
  3. Sidebar. Integrations > Private Apps.
  4. Click "Create a private app"
  5. Give it a name, e.g. "Blendbase app"
  6. Switch to "Scopes" in the header and select:
    1. crm.objects.companies - Read & Write
    2. crm.objects.contacts - Read & Write
    3. crm.objects.deals - Read & Write
  7. Click "Create app"
  8. In app view page navigate to "Access token" section and copy the Access token, store it in "Integration Secret" at http://localhost:3000/ (HUBSPOT_ACCESS_TOKEN in the .env file for development and tests).

API

APIs:

  • Connect API for managing your consumers and integrations with CRMs
  • Omni API for interacting with CRM objects like contacts, notes, deals, etc.

API authentication is done via the Authorization header which should have a JWT token encoded with the value of the BLENDBASE_AUTH_SECRET environment variable. The JWT token should have the cunsomer_id claim that represents the current Consumer on behalf of whom CRM is being called. See jwt.js for an example.

Development

DB Setup

  1. Set up Postgres database
createuser -d blendbase
createdb -O blendbase blendbase
  1. Run go run main.go db:migrate to migrate the database
  2. Run go run main.go db:seed to init DB with test data

Running the server

  1. go run main.go server
  2. Go to http://localhost:8080/ to use GraphQL Playground
  3. Execute the query

GraphQL Generation

Blendbase is using gqlgen to generate the code based on the schema located at /graph/schema.graphqls. After updating the schema make sure to run go run github.com/99designs/gqlgen generate

Sample connect React application

Sample React connect app is used to demonstrate how blendbase can be integrated into a React app.

Setup

  1. cd connect-fullstack-webapp-sample
  2. cp .env.sample .env
  3. Make sure to assigned a value for BLENDBASE_AUTH_SECRET (see API Authentication section above)
  4. yarn install - to install the dependencies
  5. Run curl http://localhost:3000/api/fetchConsumerID in directory of the React app. That will call Blendbase API and will create and new consumer. Follow the instruction in the output of the API call.
  6. yarn run dev - to run the app

Testing

make sure you have .env.test file in your root directory. You can copy .env.sample if you are doing this for the first time:

cp .env.sample .env.test

To run all the tests:

go test -v ./...

Run a specific file:

go test -v blendbase/connectors/salesforce

Disable cache:

go test -count 1 -v ...

License

Blendbase monorepo uses multiple licenses.

The license for a particular work is defined with following prioritized rules:

  1. License directly present in the file
  2. LICENSE file in the same directory as the work
  3. First LICENSE found when exploring parent directories up to the project top level directory
  4. Defaults to Elastic License 2.0

Author: blendbase
Source Code: https://github.com/blendbase/blendbase
License: View license

#graphql #saas 

Blendbase: Single Open-source GraphQL API to Connect CRMs To Your SaaS
Diego  Elizondo

Diego Elizondo

1650196260

Una Guía Paso A Paso Para Generar Y Validar Ideas De SaaS

Comenzar un negocio de SaaS o micro-SaaS puede ser un esfuerzo rentable, pero primero, debe tener la idea correcta. Entonces, ¿cómo haces para hacer eso? En esta guía paso a paso, lo guiaremos a través del proceso de encontrar y examinar posibles ideas de SaaS. También le mostraremos cómo determinar si vale la pena seguir una idea o no.

¡Empecemos!

La guía completa para generar y validar ideas de SaaS

¿Qué es SaaS?

SaaS significa software como servicio. En términos sencillos, significa crear y vender una aplicación de software a la que los clientes puedan acceder y utilizar a través de Internet.

Algunos ejemplos populares de negocios SaaS incluyen Slack, Dropbox y Salesforce.

Las empresas SaaS tienen algunas características clave que debe tener en cuenta al generar ideas:

  • Por lo general , se basan en suscripciones , lo que significa que los clientes pagan una tarifa mensual o anual para usar el software.
  • Por lo general, están basados ​​en la nube , lo que significa que se puede acceder a ellos desde cualquier lugar con una conexión a Internet.
  • A menudo tienen un modelo freemium , lo que significa que ofrecen una versión básica del software de forma gratuita y luego venden a los clientes una versión premium paga con más funciones.

Esta combinación de ingresos recurrentes, escalabilidad y generación de demanda impulsada por freemium hace que el modelo SaaS sea popular entre los empresarios.

Un proceso para generar y validar ideas de SaaS

Construir un negocio es un trabajo duro, y el riesgo de perder años en algo que no funciona puede ser desalentador. ¿La clave para evitar este desastre? Resolver problemas que las personas están motivadas a resolver en sus vidas, ¡lo suficiente como para pagar por la solución!

Hay muchas maneras diferentes de generar ideas de SaaS. Profundizaremos en estas estrategias en un momento. Pero independientemente de los métodos de generación de ideas que utilicemos, seguiremos un proceso similar desde la lluvia de ideas hasta la creación de prototipos:

  1. Haga una lista de segmentos de clientes potenciales o mercados objetivo para su negocio SaaS.
  2. Para cada mercado objetivo, haga una lluvia de ideas sobre una lista de posibles problemas o desafíos que podrían enfrentar y con los que su software podría ayudar.
  3. Una vez que tenga una lista de problemas potenciales, intente generar ideas para soluciones de software que puedan abordar esos problemas.
  4. A medida que se le ocurran ideas, comience a evaluarlas. ¿Es esta una idea que es factible para usted seguir? ¿Hay un mercado lo suficientemente grande para esto? ¿Es este un problema por el que la gente está dispuesta a pagar para que se resuelva?

Hay algunos beneficios al usar este marco:

  • Conocerá su mercado y su cliente objetivo, lo que facilita todo, desde la investigación de usuarios hasta el marketing.
  • Sabrá el problema que está resolviendo (y que es un problema legítimo que la gente tiene y que podría pagar para resolverlo).
  • Validarás que hay demanda existente en el mercado.
  • Validará su capacidad para entregar el resultado.

Esto garantiza que sus ideas se centren en el problema y que haya realizado una debida diligencia básica sobre su potencial de mercado antes de dedicar tiempo a un proceso de validación más formal.

Podemos modificar las entradas para obtener diferentes resultados, pero este es el marco al que seguiremos regresando para generar y examinar ideas de SaaS. Echemos un vistazo más de cerca a cada paso.

1. Identifica tu mercado objetivo

El primer paso es identificar quiénes podrían ser tus clientes potenciales. Los negocios de SaaS suelen servir a uno o más de los siguientes segmentos amplios:

  • Pequeñas empresas: Empresas con menos de 100 empleados.
  • Empresas empresariales: empresas con más de 100 empleados.
  • Startups: Nuevos negocios que buscan un rápido crecimiento.
  • Consumidores: Usuarios individuales que buscan una solución a un problema.

Estas son las categorías más amplias con las que tenderá a trabajar en SaaS, pero puede ser mucho más específico según el nicho, el rango de ingresos (o ingresos) y otros datos demográficos, intereses, etc.

Una vez que haya identificado algunos segmentos de clientes potenciales, comience a pensar en los problemas que podrían enfrentar.

2. Haga una lluvia de ideas sobre una lista de posibles problemas

Para cada mercado objetivo, haga una lista de los posibles problemas o desafíos que podrían enfrentar y con los que su software podría ayudar. Mientras haces una lluvia de ideas, piensa en lo siguiente:

  • ¿Cuáles son los dolores y frustraciones comunes que experimenta su mercado objetivo?
  • ¿Cuáles son sus retos del día a día?
  • ¿Cuáles son sus objetivos a largo plazo?
  • ¿Qué procesos o tareas les parecen lentos o tediosos?

Aquí hay una hoja de trucos para iniciar su lluvia de ideas basada en los segmentos de mercado de nivel superior.

Algunos problemas comunes que enfrentan las pequeñas empresas incluyen:

  • La gestión del inventario
  • Contabilidad y teneduría de libros
  • Gestión de las relaciones con los clientes (CRM)
  • Recursos humanos (RRHH)
  • Márketing

Los desafíos comunes que enfrentan las empresas incluyen:

  • Gestión y almacenamiento de datos
  • Seguridad
  • Cumplimiento
  • integraciones
  • Eficiencia del flujo de trabajo

Los problemas que enfrentan las startups incluyen:

  • Rápido crecimiento y escalado
  • Contratación e incorporación
  • Financiación
  • Adquisición de clientes

Y finalmente, algunos problemas comunes que enfrentan los consumidores :

  • Productividad
  • Salud y Belleza
  • Organización y gestión del tiempo.

Ahora que tiene una lista de problemas potenciales, es hora de comenzar a pensar en soluciones de software que puedan abordar esos problemas.

3. Generar ideas para soluciones

A medida que genere ideas, tenga en cuenta que las mejores soluciones SaaS suelen ser las que:

  • Simple: Resuelven el problema de una manera directa sin campanas ni silbatos.
  • Escalables: se pueden ampliar o reducir fácilmente para satisfacer las necesidades de diferentes clientes.
  • Flexibles: Se pueden personalizar para satisfacer las necesidades específicas de diferentes clientes.

Si está buscando más ideas, aquí hay algunos recursos para ayudarlo a comenzar:

  • The Lean Product Playbook : este libro trata sobre el uso de ideas de Lean Startup para validar sus ideas comerciales de manera rápida y eficiente, y encontrar rápidamente el producto-mercado adecuado con productos mínimos viables y comentarios rápidos de los clientes.
  • The Lean Startup : este libro de Eric Ries es una pieza fundamental que le dará una comprensión del marco más amplio de Lean Startup.
  • Los subreddits r/SaaS y r/SideProject : este subreddit está lleno de emprendedores que buscan ideas para su próximo negocio SaaS.
  • GummySearch: Hablando de Reddit, esta herramienta te ayuda a investigar la audiencia de Reddit para obtener información. Descubra a qué problemas se enfrentan sus clientes y cómo quieren que se les aborde.
  • IndieHackers: este sitio es una mina de oro para estudios de casos de exitosos empresarios de SaaS (así como una gran comunidad de creadores).

Una vez que haya generado una lista de ideas potenciales, es hora de comenzar a evaluarlas.

4. Evalúa tus ideas

Cuando esté pensando en qué tendencias capitalizar, es importante considerar si existe un mercado lo suficientemente grande para su idea.

Este paso intermedio entre generar ideas y validarlas es fundamental. La validación requiere una inversión de tiempo breve pero notable, por lo que nunca podrá validar todas las ideas que tenga.

Esto lo ayudará a reducir su lista de ideas hasta el ganador final sin depender demasiado de la pura intuición. No querrás descartar prematuramente una idea profética, pero tampoco querrás atascarte aquí.

La idea es filtrar rápidamente una lista corta para un proceso de validación más formal.

Al evaluar sus ideas, hay algunos factores clave que querrá tener en cuenta:

  • ¿Es esta una idea que es factible para usted seguir?
  • ¿Hay un mercado lo suficientemente grande para esto?
  • ¿Es este un problema por el que la gente está dispuesta a pagar para que se resuelva?

Una forma útil de guiar sus decisiones con datos rápidamente es usar el Planificador de palabras clave de Google o su herramienta de investigación de palabras clave preferida. vea cuántas personas están buscando palabras clave relacionadas con su idea.

Esto le permite ver cuántas personas están buscando soluciones en un área determinada de una manera rápida y aproximada, pero basada en datos. Si no hay suficientes personas buscando estas palabras clave, es probable que no haya un mercado lo suficientemente grande para su idea.

El beneficio de usar este indicador es que comenzará con una idea que ha sido evaluada para la demanda y que sabe que tiene una estrategia de búsqueda orgánica viable disponible.

Volveremos a la validación a gran escala más adelante.

Entradas para la ideación de SaaS

Cuando se trata de generar ideas para productos SaaS, existen algunos métodos que puede utilizar.

Entre otros, puede resolver su propio problema, preguntar a sus clientes potenciales o realizar estudios de mercado.

Podría ayudar pensar en estos lentes de lluvia de ideas en términos de la perspectiva que enfatizan.

  • Los métodos centrados en el ser humano analizan los problemas que las personas experimentan directamente y tratan de llenar el vacío con una solución innovadora. Estos enfoques implican mucha conversación y trabajo en red.
    • Busque problemas en su propia vida o trabajo que puedan resolverse con una aplicación de software.
    • Hable con personas que conoce y pregúnteles si alguna vez se han enfrentado a situaciones frustrantes o desafíos en los que una aplicación de software podría ayudar.
    • Y hable con personas que trabajan en ventas o servicio al cliente. Pregúnteles qué desafíos enfrentan sus clientes con los que podría ayudar una aplicación.
  • Los métodos centrados en el mercado evalúan el mercado tal como está hoy, cualquiera que sea el mercado.
    • Investigación de mercado. ¿Quiénes son los jugadores establecidos? ¿Quiénes son los disruptores? ¿Cuánto dinero se gana actualmente en la industria? ¿Existen factores tangibles que indiquen un crecimiento continuo o renovado en los próximos años?
    • ¿Cuánta oportunidad accesible queda en esta industria en su etapa actual de madurez, suponiendo que no cambien otros factores?
    • Si otros factores interrumpen ese mercado (como un cambio en la tecnología), ¿cuáles podrían ser? ¿Y puedes sacar provecho de esa predicción?
    • Identifique las tendencias en la industria de la tecnología y piense en cómo podría crear un negocio SaaS para capitalizarlas. Esté atento a las noticias sobre empresas que han sido financiadas o adquiridas.
  • Los métodos centrados en el negocio analizan la forma en que opera cada negocio que constituye "la competencia". La idea es encontrar una forma lo suficientemente sustancial de mejorar las deficiencias para darle un caso de negocios.
    • Busque empresas de SaaS que tengan dificultades e intente averiguar por qué. ¿Es falta de demanda o una mala experiencia en el producto que puedes mejorar?
    • Esto también puede darle una idea de lo que no funciona en la industria y ayudarlo a evitar cometer los mismos errores.
    • Busque empresas de SaaS que hayan superado las expectativas de crecimiento o financiación. ¿Puede replicar su enfoque disruptivo en su nicho o de una manera que aproveche sus fortalezas?

Cada uno de estos métodos puede ser una excelente manera de generar ideas para productos SaaS que podrían tener éxito.

1. Resuelve tu propio problema

Uno de los mejores métodos es observar los problemas que está experimentando en su propio negocio o en su vida.

  • ¿Cuáles son los dolores y frustraciones que estás experimentando?
  • ¿Qué procesos o tareas considera que consumen mucho tiempo o son tediosos?

También puede mirar los productos SaaS que está utilizando actualmente y ver si se podría mejorar alguna área.

  • ¿Qué características te faltan?
  • ¿Qué procesos o tareas considera que consumen mucho tiempo o son tediosos?

Estas podrían ser áreas potenciales en las que centrarse cuando se presenten ideas de SaaS.

Basecamp es un gran ejemplo de una empresa que resolvió su propio problema. Creó una herramienta de gestión de proyectos porque estaban frustrados con las opciones existentes en el mercado.

Si experimenta puntos débiles en su negocio o en su vida, vale la pena considerar si una aplicación de software podría ayudar a resolverlos.

2. Pregunta a tus clientes

Otro método es hablar con personas que conoces y preguntarles sobre sus experiencias. Esta puede ser una excelente manera de generar ideas porque obtiene información de primera mano sobre los desafíos que enfrentan las personas.

Habla con otros dueños de negocios y pregúntales sobre los problemas que están experimentando. También puede mirar los productos SaaS que están usando actualmente y ver si se podría mejorar alguna área.

Zapier es un gran ejemplo de una empresa que pidió ideas a sus clientes. Crearon una herramienta que permite a las personas automatizar tareas porque constantemente se les pedía recomendaciones sobre la mejor manera de hacerlo.

Otro método es hablar con personas que trabajan en ventas o servicio al cliente. 

Estas son las personas que están en primera línea y tienen contacto directo con los clientes.

Es probable que estén al tanto de cualquier punto débil o frustración que experimenten los clientes. Esta información se puede utilizar para generar ideas para productos SaaS que podrían ayudar a resolver estos problemas.

3. Realizar estudios de mercado

Realice estudios de mercado y vea qué áreas están creciendo o disminuyendo. Esto puede ayudarlo a identificar áreas potenciales en las que concentrarse cuando se le ocurran ideas de SaaS.

Puede ver informes de la industria, leer artículos y hablar con personas de la industria para comprender mejor lo que está sucediendo.

Slack es un gran ejemplo de una empresa que vio una oportunidad en el mercado y creó un producto para capitalizarla. Se dieron cuenta de que las personas usaban aplicaciones de mensajería para el trabajo y vieron la oportunidad de crear una herramienta específica para las empresas.

4. Tendencias de investigación en la industria tecnológica

Identifique las tendencias en la industria de la tecnología y piense en cómo podría crear un negocio SaaS para capitalizarlas.

Esta puede ser una excelente manera de generar ideas porque puede ver lo que es popular e identificar las brechas en el mercado.

En la sección anterior, pensamos en negocios potenciales al evaluar el panorama competitivo en verticales y nichos. 

Aquí, estamos empleando métodos similares de una manera diferente. ¿Qué desarrollos en la industria de la tecnología abren la puerta a nuevas soluciones para viejos problemas en las industrias existentes? Estamos particularmente interesados ​​en aplicar estos amplios cambios y desarrollos a las áreas que hemos identificado como aquellas en las que tenemos habilidades, fortalezas y experiencia.

Esté atento a las noticias sobre empresas que han sido financiadas o adquiridas. Esto puede ser una buena indicación de que existe demanda para el tipo de producto que ofrecen. Incluso si es demasiado tarde para tener una alta probabilidad de éxito en uno de estos mercados, a menudo hay brechas adyacentes que puede llenar o formas de reutilizar la estrategia para un segmento más específico.

  • Puede consultar publicaciones tecnológicas populares como TechCrunch y VentureBeat para mantenerse actualizado sobre las últimas tendencias de la industria.
  • También puede usar herramientas como Google Trends y Trends Everywhere para ver qué palabras clave se buscan más en una escala relativa. Esto puede brindarle información sobre lo que les interesa a las personas y para lo que podrían estar buscando soluciones.
  • El boletín Trends ha hecho un negocio exitoso al sacar a la superficie oportunidades basadas en tendencias para emprendedores.
  • Para obtener una visión más detallada de las próximas tendencias, consulte Hype Cycle for Emerging Technologies de Gartner .

Una vez que haya identificado algunas tendencias que cree que tienen potencial, es hora de comenzar a pensar en cómo podría crear un negocio SaaS para capitalizarlas.

Por ejemplo:

  • Si está interesado en la IA, puede generar ideas para un negocio de SaaS que use API de IA para ayudar a las empresas a automatizar tareas o tomar mejores decisiones.
  • Si está interesado en los chatbots, puede crear un negocio SaaS que ayude a las empresas a filtrar y alimentar su actividad de servicio al cliente en el modelo de una manera más efectiva.

Validación de sus ideas

Para ayudarlo a evaluar sus ideas, aquí hay un marco simple que puede usar. Este marco se basa en la metodología Lean Startup, que se trata de validar sus ideas de negocios de manera rápida y eficiente.

El primer paso es validar que existe un problema real a resolver y la demanda existente en el mercado . Puede hacer esto realizando estudios de mercado y hablando con clientes potenciales.

Una vez que haya validado que hay demanda, el siguiente paso es validar su capacidad para construir la idea . Aquí es donde deberá realizar una investigación de factibilidad para ver si es posible crear una solución que satisfaga las necesidades de sus clientes.

El siguiente paso es validar que su solución resuelve el problema . Puede hacer esto realizando una investigación de usuarios y hablando con clientes potenciales. Durante sus conversaciones, pregunte:

  • ¿Cuáles son los dolores y frustraciones comunes que experimenta su mercado objetivo?
  • ¿Cuáles son sus retos del día a día?
  • ¿Cuáles son sus objetivos a largo plazo?
  • ¿Qué procesos o tareas les parecen lentos o tediosos?

Luego, valide que las personas estén dispuestas a pagar por su solución. Puede hacer esto realizando una investigación de clientes y hablando con clientes potenciales. Querrás saber:

  • ¿Cuánto están gastando actualmente para resolver este problema?
  • ¿Cuánto estarían dispuestos a pagar por una solución?

Una vez que haya validado que existe un mercado para su idea, el siguiente paso es validar su modelo de negocio , su estrategia de comercialización y su estrategia de crecimiento. Hay algunas maneras diferentes de hacer esto:

  • Cree una página de destino: esta es una excelente manera de validar su idea de manera rápida y eficiente. Puede crear una página de destino simple con un formulario de registro de correo electrónico. Luego, puede dirigir el tráfico a su página de destino a través de anuncios pagados o redes sociales.
  • Lanzar un producto mínimo viable: esta es una excelente manera de validar su idea con clientes potenciales. Puede lanzar una versión simple de su producto y ver cómo reacciona la gente. Si las personas están dispuestas a usar su producto, entonces sabrá que tiene un modelo comercial viable.
  • Cree una plataforma de ventas: esta es una excelente manera de validar su idea con clientes potenciales. Puede crear una plataforma de ventas simple y usarla para presentar su producto a clientes potenciales. Si las personas están interesadas en su producto, entonces sabe que tiene una estrategia de comercialización viable.
  • Lanza un MVP o beta: esta es una excelente manera de validar tu idea con clientes potenciales. Puede lanzar una versión beta de su producto y ver cómo reacciona la gente. Si las personas están dispuestas a usar su producto, entonces sabrá que tiene una estrategia de comercialización viable.
  • Realice entrevistas con los clientes: esta es una excelente manera de validar su idea con clientes potenciales. Puede realizar entrevistas con los clientes y ver cómo reacciona la gente. Si las personas están interesadas en su producto, entonces sabe que tiene una estrategia de crecimiento viable.
  • Lanza una campaña de anuncios pagados: esta es una excelente manera de validar tu idea con clientes potenciales. Puede lanzar una campaña de anuncios pagados y ver cómo reacciona la gente. Si las personas están interesadas en su producto, entonces sabe que tiene una estrategia de crecimiento viable.

Después de haber pasado por este proceso, debe tener una buena idea de si su idea de SaaS es viable o no. Si puede validar tanto la demanda como la viabilidad, entonces tiene buenas posibilidades de éxito. Sin embargo, si solo puede validar uno u otro, reconsidere su idea.

Una nota final: es importante validar sus ideas de la manera más rápida y eficiente posible. El objetivo no es pasar meses o años desarrollando un producto que nadie quiere. El objetivo es validar sus ideas rápidamente para que pueda pasar a la siguiente idea o continuar desarrollándola.

Conclusión

La industria de la tecnología está en constante evolución y puede ser difícil mantenerse al día con las últimas tendencias. Sin embargo, si está interesado en iniciar un negocio de SaaS, es importante investigar y validar su idea antes de invertir demasiado tiempo o dinero en ella.

En este artículo, describimos algunos consejos para generar ideas para un negocio SaaS y cómo validarlos. Con un poco de investigación, puede estar en camino de iniciar un negocio SaaS exitoso.

Fuente: https://www.sitepoint.com/generating-saas-ideas/

#saas 

Una Guía Paso A Paso Para Generar Y Validar Ideas De SaaS
高橋  花子

高橋 花子

1650109622

SaaSのアイデアを生成および検証するためのステップバイステップガイド

SaaSビジネスまたはマイクロSaaSを開始することは、有益な取り組みになる可能性がありますが、最初に、正しいアイデアを考え出す必要があります。では、どうやってそれを行うのですか?このステップバイステップガイドでは、潜在的なSaaSのアイデアを見つけて検証するプロセスについて説明します。また、アイデアを追求する価値があるかどうかを判断する方法についても説明します。

始めましょう!

SaaSのアイデアを生成および検証するための完全なガイド

SaaSとは何ですか?

SaaSは、Software-as-a-Serviceの略です。素人の言葉で言えば、インターネットを介して顧客がアクセスして使用できるソフトウェアアプリケーションを作成して販売することを意味します。

SaaSビジネスの一般的な例には、Slack、Dropbox、Salesforceなどがあります。

SaaSビジネスには、アイデアを生み出すときに留意すべきいくつかの重要な特徴があります。

  • これらは通常、サブスクリプションベースです。つまり、顧客はソフトウェアを使用するために月額または年額の料金を支払います。
  • これらは通常クラウドベースです。つまり、インターネット接続があればどこからでもアクセスできます。
  • 彼らはしばしばフリーミアムモデルを持っています。つまり、彼らはソフトウェアの基本バージョンを無料で提供し、それから顧客をより多くの機能を備えた有料のプレミアムバージョンにアップセルします。

経常収益、スケーラビリティ、およびフリーミアム主導の需要生成のこの組み合わせにより、SaaSモデルは起業家の間で人気があります。

SaaSのアイデアを生成および検証するためのプロセス

ビジネスを構築することは大変な作業であり、うまくいかないものに何年も浪費するリスクは気が遠くなる可能性があります。この災害を回避するための鍵は?人々が自分たちの生活の中で解決しようと動機付けられている問題を解決する-彼らが解決策にお金を払うのに十分です!

SaaSのアイデアを生み出すにはさまざまな方法があります。これらの戦略については、すぐに詳しく説明します。ただし、使用するアイデア生成方法に関係なく、ブレーンストーミングからプロトタイピングまで同様のプロセスに従います。

  1. SaaSビジネスの潜在的な顧客セグメントまたはターゲット市場のリストを作成します。
  2. ターゲット市場ごとに、ソフトウェアが役立つ可能性のある潜在的な問題や課題のリストをブレインストーミングします。
  3. 潜在的な問題のリストができたら、それらの問題に対処できるソフトウェアソリューションのアイデアを考え出すようにしてください。
  4. アイデアを思いついたら、それらの評価を開始します。これはあなたが追求するのに実行可能なアイデアですか?これに十分な大きさの市場はありますか?これは、人々が解決するために喜んで支払う問題ですか?

このフレームワークを使用することには、いくつかの利点があります。

  • あなたはあなたの市場とターゲット顧客を知るでしょう、それはユーザー調査からマーケティングまですべてをより簡単にします。
  • あなたはあなたが解決している問題を知っているでしょう(そしてそれは人々が抱えている正当な問題であり、解決するためにお金を払うかもしれません)。
  • 市場に既存の需要があることを検証します。
  • 結果を提供する能力を検証します。

これにより、アイデアに問題が集中し、より正式な検証プロセスに時間を費やす前に、市場の可能性について基本的なデューデリジェンスを行うことができます。

入力を変更してさまざまな結果を得ることができますが、これは、SaaSのアイデアを生成および検証するために引き続き使用するフレームワークです。各ステップを詳しく見ていきましょう。

1.ターゲット市場を特定する

最初のステップは、潜在的な顧客が誰であるかを特定することです。SaaSビジネスは通常、次の幅広いセグメントの1つ以上にサービスを提供します。

  • 中小企業:従業員が100人未満の企業。
  • エンタープライズビジネス: 100人以上の従業員を抱えるビジネス。
  • スタートアップ:急成長を求めている新規事業。
  • 消費者:問題の解決策を探している個々のユーザー。

これらは、SaaSで使用する傾向のある最も幅広いカテゴリですが、ニッチ、収入(または収益)の範囲、その他の人口統計、関心などに基づいて、はるかに具体的にすることができます。

いくつかの潜在的な顧客セグメントを特定したら、彼らが直面する可能性のある問題のブレインストーミングを開始します。

2.潜在的な問題のリストをブレインストーミングします

ターゲット市場ごとに、ソフトウェアが役立つ可能性のある潜在的な問題や課題のリストを作成します。ブレーンストーミングするときは、次のことを考えてください。

  • ターゲット市場が経験する一般的な苦痛と欲求不満は何ですか?
  • 彼らの日々の課題は何ですか?
  • 彼らの長期的な目標は何ですか?
  • 彼らはどのようなプロセスやタスクに時間と手間を感じますか?

これは、トップレベルの市場セグメントに基づいてブレインストーミングを開始するためのチートシートです。

中小企業が直面するいくつかの一般的な問題は次のとおりです。

  • 在庫管理
  • 会計と簿記
  • 顧客関係管理(CRM)
  • 人材(HR)
  • マーケティング

エンタープライズビジネスが直面する一般的な課題は次のとおりです。

  • データ管理とストレージ
  • 安全
  • コンプライアンス
  • 統合
  • ワークフローの効率

スタートアップが直面する問題は次のとおりです。

  • 急速な成長とスケーリング
  • 採用とオンボーディング
  • 資金調達
  • 顧客獲得

そして最後に、消費者が直面するいくつかの一般的な問題:

  • 生産性
  • 健康と運動
  • 組織と時間の管理

潜在的な問題のリストができたので、次はそれらの問題に対処できるソフトウェアソリューションについて考え始めます。

3.ソリューションのアイデアを生成します

アイデアを生み出すときは、通常、最高のSaaSソリューションは次のようなものであることに注意してください。

  • シンプル:ベルやホイッスルを使わずに、簡単な方法で問題を解決します。
  • スケーラブル:さまざまな顧客のニーズに合わせて、簡単にスケールアップまたはスケールダウンできます。
  • 柔軟性:さまざまな顧客の特定のニーズに合わせてカスタマイズできます。

より多くのアイデアを探している場合は、始めるのに役立ついくつかのリソースがあります。

  • リーン製品プレイブックこの本は、リーンスタートアップのアイデアを使用して、ビジネスアイデアを迅速かつ効率的に検証し、MinimumViableProductsと迅速な顧客フィードバックで製品市場の適合性をすばやく見つけることを目的としています。
  • リーンスタートアップエリックリースによるこの本は、より広範なリーンスタートアップフレームワークの理解を与える独創的な作品です。
  • r/SaaSおよびr/SideProjectサブレディット:このサブレディットは、次のSaaSビジネスのアイデアを探している起業家でいっぱいです。
  • GummySearch: Redditと言えば、このツールは、洞察を得るためにRedditオーディエンス調査をマイニングするのに役立ちます。顧客が直面している問題と、顧客がどのように対処したいかを発見します。
  • IndieHackers:このサイトは、成功したSaaS起業家(および優れたメーカーコミュニティ)のケーススタディの金鉱です。

考えられるアイデアのリストを作成したら、次にそれらの評価を開始します。

4.アイデアを評価します

どのトレンドを活用するかを考えるときは、アイデアに十分な大きさの市場があるかどうかを検討することが重要です。

アイデアを考え出すこととアイデアを検証することの間のこの中間ステップは不可欠です。検証には短いがかなりの時間の投資が必要であるため、あなたが持っているすべてのアイデアを検証することは決してできません。

これにより、直感に頼ることなく、アイデアのリストを最終的な勝者に絞り込むことができます。先見の明のあるアイデアを時期尚早に破棄したくはありませんが、ここでも行き詰まりたくはありません。

アイデアは、より正式な検証プロセスのために候補リストをすばやくフィルタリングすることです。

アイデアを評価するとき、覚えておきたい重要な要素がいくつかあります。

  • これはあなたが追求するのに実行可能なアイデアですか?
  • これに十分な大きさの市場はありますか?
  • これは、人々が解決するために喜んで支払う問題ですか?

急いでデータを使って決定を導くための便利な方法の1つは、Googleキーワードプランナーまたはお好みのキーワード調査ツールを使用することです。あなたのアイデアに関連するキーワードを検索している人の数を確認してください。

これにより、特定の領域でソリューションを探している人の数を、高速かつ大まかな方法​​で、ただしデータ駆動型で確認できます。これらのキーワードを検索する人が十分でない場合は、あなたのアイデアに十分な市場がない可能性があります。

このインジケーターを使用する利点は、需要について評価されたアイデアから始めて、実行可能なオーガニック検索戦略を利用できることがわかっていることです

後で本格的な検証に戻ります。

SaaSアイデアへのインプット

SaaS製品のアイデアを生み出すことになると、使用できる方法がいくつかあります。

とりわけ、あなたはあなた自身の問題を解決するか、あなたの潜在的な顧客に尋ねるか、または市場調査を行うことができます。

これらのブレーンストーミングレンズを、それらが強調する視点の観点から考えると役立つ場合があります。

  • 人間に焦点を当てた方法は、人々が直接経験している問題を見て、革新的な解決策でギャップを埋めようとします。これらのアプローチには、多くの会話とネットワーキングが含まれます。
    • ソフトウェアアプリケーションで解決できる、自分の生活や仕事の問題を探します。
    • 知っている人と話をして、ソフトウェアアプリケーションが役立つ可能性のある苛立たしい状況や課題に直面したことがあるかどうかを尋ねます。
    • そして、営業やカスタマーサービスで働く人々と話してください。アプリが役立つ可能性のある、顧客が直面している課題を尋ねます。
  • 市場に焦点を当てた方法は、現在の市場を評価します。
    • 市場調査。確立されたプレーヤーは誰ですか?破壊者は誰ですか?今日、業界全体でどのくらいのお金が稼がれていますか?また、今後数年間の継続的または新たな成長を示す具体的な要因はありますか?
    • 他の要因が変わらないと仮定して、成熟の現在の段階でこの業界にどれだけのアクセス可能な機会が残っていますか?
    • 他の要因(テクノロジーの変化など)がその市場を混乱させる場合、それらは何でしょうか?そして、あなたはその予測を利用できますか?
    • テクノロジー業界のトレンドを特定し、それらを活用するためにSaaSビジネスを作成する方法を考えてください。資金提供または買収された企業に関するニュース記事に目を離さないでください。
  • ビジネスに焦点を当てた方法は、「競争」を構成する各ビジネスの運営方法を調べます。アイデアは、ビジネスケースを提供するために欠点を改善するための実質的な十分な方法を見つけることです。
    • 苦労しているSaaS企業を探し、その理由を理解しようとします。それは需要の欠如ですか、それともあなたが改善できる製品の貧弱な経験ですか?
    • これにより、業界で機能しないものについての洞察が得られ、同じ過ちを犯さないようにすることができます。
    • 成長や資金調達への期待を上回っているSaaS企業を探してください。あなたのニッチで、またはあなたの強みを生かす方法で、彼らの破壊的なアプローチを再現できますか?

これらの各方法は、成功する可能性のあるSaaS製品のアイデアを生み出すための優れた方法です。

1.自分の問題を解決する

最良の方法の1つは、自分のビジネスや生活で経験している問題を調べることです。

  • あなたが経験している痛みや欲求不満は何ですか?
  • どのようなプロセスやタスクに時間と手間がかかりますか?

また、現在使用しているSaaS製品を調べて、改善できる領域があるかどうかを確認することもできます。

  • どの機能が欠けていますか?
  • どのようなプロセスやタスクに時間と手間がかかりますか?

これらは、SaaSのアイデアを思いつくときに焦点を当てる可能性のある領域である可能性があります。

Basecampは、自社の問題を解決した企業の好例です。市場に出回っている既存のオプションに不満を感じていたため、プロジェクト管理ツールを作成しました。

ビジネスや生活で問題が発生している場合は、ソフトウェアアプリケーションが問題の解決に役立つかどうかを検討する価値があります。

2.顧客に尋ねる

もう一つの方法は、あなたが知っている人と話し、彼らの経験について尋ねることです。これは、人々が直面している課題に関する直接的な情報を取得しているため、アイデアを生み出すための優れた方法になります。

他の事業主と話し、彼らが経験している問題について彼らに尋ねてください。また、現在使用しているSaaS製品を調べて、改善できる領域があるかどうかを確認することもできます。

Zapierは、顧客にアイデアを求めた企業の好例です。彼らは、これを行うための最良の方法に関する推奨事項を常に求められていたため、人々がタスクを自動化できるツールを作成しました。

もう1つの方法は、営業やカスタマーサービスで働く人々と話すことです。 

これらは最前線にいて、顧客と直接接触している人々です。

彼らは、顧客が経験している問題点やフラストレーションに気付いている可能性があります。この情報は、これらの問題の解決に役立つ可能性のあるSaaS製品のアイデアを生成するために使用できます。

3.市場調査を実施する

市場調査を実施し、どの分野が成長または衰退しているかを確認します。これは、SaaSのアイデアを思いつくときに焦点を当てる可能性のある領域を特定するのに役立ちます。

業界のレポートを見たり、記事を読んだり、業界の人々と話をしたりして、何が起こっているのかをよりよく理解することができます。

Slackは、市場でチャンスを見出し、それを活用する製品を作成した企業の好例です。彼らは、人々が仕事にメッセージングアプリを使用していることに気づき、ビジネス専用のツールを作成する機会を見ました。

4.テクノロジー業界の研究動向

テクノロジー業界のトレンドを特定し、それらを活用するためにSaaSビジネスを作成する方法を考えてください。

人気のあるものを確認し、市場のギャップを特定できるため、これはアイデアを生み出すための優れた方法です。

前のセクションでは、業種とニッチの競争環境を評価することにより、潜在的なビジネスについて考えました。 

ここでは、同様の方法を別の方法で採用しています。テクノロジー業界のどの開発が、既存の業界の古い問題に対する新しい解決策への扉を開きますか?私たちは、これらの幅広い変化と発展を、私たちがスキル、強み、経験を持っている分野として特定した分野に適用することに特に関心を持っています。

資金提供または買収された企業に関するニュース記事に目を離さないでください。これは、彼らが提供する製品のタイプに対する需要があることを示す良い兆候である可能性があります。これらの市場のいずれかで成功する可能性が高いのに遅すぎる場合でも、多くの場合、埋めることができる隣接するギャップや、より具体的なセグメントの戦略を再利用する方法があります。

  • TechCrunchVentureBeatなどの人気のある技術出版物をチェックして、最新の業界トレンドを入手することができます。
  • また、 Googleトレンドやトレンドエブリウェアなどのツールを使用して、相対的なスケールで最も検索されているキーワードを確認することもできます。これにより、人々が何に興味を持ち、解決策を探しているのかについての洞察を得ることができます。
  • トレンドニュースレターは、起業家のためのトレンドベースの機会を浮き彫りにすることでビジネスを成功させました。
  • 今後のトレンドの詳細については、GartnerのEmergingTechnologiesのHypeCycleをご覧ください。

可能性があると思われるいくつかのトレンドを特定したら、それを活用するためにSaaSビジネスを作成する方法について考え始めます。

例えば:

  • AIに興味がある場合は、AI APIを使用してビジネスがタスクを自動化したり、より適切な意思決定を行ったりするのに役立つSaaSビジネスのアイデアをブレインストーミングできます。
  • チャットボットに興味がある場合は、企業がより効果的な方法で顧客サービス活動をフィルタリングしてモデルにフィードするのに役立つSaaSビジネスを作成できます。

アイデアの検証

アイデアを評価するのに役立つように、使用できる簡単なフレームワークを次に示します。このフレームワークは、ビジネスアイデアを迅速かつ効率的に検証することを目的としたリーンスタートアップの方法論に基づいています。

最初のステップは、解決すべき実際の問題と、市場の既存の需要があることを検証することです。これを行うには、市場調査を実施し、潜在的な顧客と話をします。

需要があることを確認したら、次のステップは、アイデアを構築する能力を検証することです。ここで、顧客のニーズを満たすソリューションを構築できるかどうかを確認するために、いくつかの実現可能性調査を行う必要があります。

次のステップは、ソリューションが問題を解決することを検証することです。これを行うには、ユーザー調査を実施し、潜在的な顧客と話をします。会話中に、次のことを尋ねます。

  • ターゲット市場が経験する一般的な苦痛と欲求不満は何ですか?
  • 彼らの日々の課題は何ですか?
  • 彼らの長期的な目標は何ですか?
  • 彼らはどのようなプロセスやタスクに時間と手間を感じますか?

次に、人々があなたのソリューションに喜んでお金を払うことを確認します。これを行うには、顧客調査を実施し、潜在的な顧客と話をします。あなたは知りたいでしょう:

  • 彼らは現在、この問題を解決するためにいくら費やしていますか?
  • 彼らは解決策にいくら払っても構わないと思っていますか?

アイデアの市場があることを検証したら、次のステップは、ビジネスモデル、市場開拓戦略、および成長戦略を検証することです。これを行うには、いくつかの異なる方法があります。

  • ランディングページを作成する:これは、アイデアをすばやく効率的に検証するための優れた方法です。メール登録フォームを使用して、簡単なランディングページを作成できます。次に、有料広告またはソーシャルメディアを介してランディングページにトラフィックを誘導できます。
  • 最小限の実行可能な製品を立ち上げる:これは、潜在的な顧客とのアイデアを検証するための優れた方法です。製品の簡単なバージョンを起動して、人々がどのように反応するかを確認できます。人々があなたの製品を喜んで使用するなら、あなたはあなたが実行可能なビジネスモデルを持っていることを知っています。
  • セールスデッキを作成する:これは、潜在的な顧客とのアイデアを検証するための優れた方法です。簡単なセールスデッキを作成し、それを使用して潜在的な顧客に製品を売り込むことができます。人々があなたの製品に興味を持っているなら、あなたはあなたが実行可能な市場開拓戦略を持っていることを知っています。
  • MVPまたはベータ版を立ち上げる:これは、潜在的な顧客とのアイデアを検証するための優れた方法です。製品のベータ版を起動して、人々がどのように反応するかを確認できます。人々があなたの製品を喜んで使用するなら、あなたはあなたが実行可能な市場開拓戦略を持っていることを知っています。
  • 顧客インタビューの実施:これは、潜在的な顧客とのアイデアを検証するための優れた方法です。顧客インタビューを実施して、人々がどのように反応するかを見ることができます。人々があなたの製品に興味を持っているなら、あなたはあなたが実行可能な成長戦略を持っていることを知っています。
  • 有料広告キャンペーンを開始する:これは、潜在的な顧客とのアイデアを検証するための優れた方法です。有料広告キャンペーンを開始して、人々がどのように反応するかを見ることができます。人々があなたの製品に興味を持っているなら、あなたはあなたが実行可能な成長戦略を持っていることを知っています。

このプロセスを経た後、SaaSのアイデアが実行可能かどうかについての良いアイデアが得られるはずです。需要と実現可能性の両方を検証できれば、成功する可能性は十分にあります。ただし、どちらか一方しか検証できない場合は、アイデアを再検討してください。

最後に、アイデアをできるだけ迅速かつ効率的に検証することが重要です。目標は、誰も望まない製品の開発に数か月または数年を費やすことではありません。目標は、アイデアをすばやく検証して、次のアイデアに進むか、アイデアの開発を続けることができるようにすることです。

結論

テクノロジー業界は絶えず進化しており、最新のトレンドに追いつくのは難しい場合があります。ただし、SaaSビジネスの開始に関心がある場合は、時間やお金をかけすぎる前に、調査を行い、アイデアを検証することが重要です。

この記事では、SaaSビジネスのアイデアを考え出すためのいくつかのヒントとそれらを検証する方法について概説しました。少し調べれば、SaaSビジネスを成功させるための道を歩むことができます。

ソース:https ://www.sitepoint.com/generated-saas-ideas/ 

#saas 

SaaSのアイデアを生成および検証するためのステップバイステップガイド

Learn about SaaS Design Principles with Kubernetes

It seems like nowadays, every company is a SaaS company. We’ve even begun stratifying by what is sold, replacing the “software” in SaaS to whatever the product’s core competency is, search-as-a-service, chat-as-a-service, video-as-a-service. This post explores how we deployed Teleport Cloud on Kubernetes, deliberately designing for maximizing security, minimizing resource consumption, and self-service.

#kubernetes #saas 

Learn about SaaS Design Principles with Kubernetes

FlaskSaas: Build Your SaaS in Flask & Python with Stripe

flaskSaas

A fork of Max Halford's flask-boilerplate. I've noticed SaaS bootstraps/boilerplates being sold upwards of $1,000 per year and I think that's fucking ridiculous. This project will be my attempt to make a great starting point for your next big business as easy and efficent as possible.

If you're here because of Siraj's video, welcome!

Features

  •  User account sign up, sign in, password reset, all through asynchronous email confirmation.
  •  Form generation.
  •  Error handling.
  •  HTML macros and layout file.
  •  "Functional" file structure.
  •  Python 3.x compliant.
  •  Asynchronous AJAX calls.
  •  Administration panel.
  •  Logging.
  •  Stripe subscriptions. (WIP)
  •  RESTful API for payments.
  •  Simple RESTful API to communicate with your app.

Libraries

Backend

Frontend

Structure

I did what most people recommend for the application's structure. Basically, everything is contained in the app/ folder.

  • There you have the classic static/ and templates/ folders. The templates/ folder contains macros, error views and a common layout.
  • I added a views/ folder to separate the user and the website logic, which could be extended to the the admin views.
  • The same goes for the forms/ folder, as the project grows it will be useful to split the WTForms code into separate files.
  • The models.py script contains the SQLAlchemy code, for the while it only contains the logic for a users table.
  • The toolbox/ folder is a personal choice, in it I keep all the other code the application will need.
  • Management commands should be included in manage.py. Enter python manage.py -? to get a list of existing commands.
  • I added a Makefile for setup tasks, it can be quite useful once a project grows.

Setup

Vanilla

  • Install the requirements and setup the development environment.

make install && make dev

  • Create the database.

python manage.py initdb

  • Run the application.

python manage.py runserver

  • Navigate to localhost:5000.

Configuration

The goal is to keep most of the application's configuration in a single file called config.py. I added a config_dev.py and a config_prod.py who inherit from config_common.py. The trick is to symlink either of these to config.py. This is done in by running make dev or make prod.

I have included a working Gmail account to confirm user email addresses and reset user passwords, although in production you should't include the file if you push to GitHub because people can see it. The same goes for API keys, you should keep them secret. You can read more about secret configuration files here.

Read this for information on the possible configuration options.

Download Details:
Author: alectrocute
Source Code: https://github.com/alectrocute/flaskSaaS
License: MIT License

#flask #python #saas 

FlaskSaas: Build Your SaaS in Flask & Python with Stripe
Daron  Moore

Daron Moore

1640923200

A Website That Makes Use Of The Specs API (glasses) Methods

SpecsOn website

😎 A website that makes use of the specs API (glasses 👓) methods. With a SAAS kind of feel to it. 🌱 

Demo · Report Bug · Request Feature

 

About The Project

SpecsOn Demo Gif

Built With

Getting Started

To get a local copy just follow these simple example steps.

Prerequisites

  • A Code Editor of your choice
I use VSCode

Installation

  1. Clone the repo
git clone https://github.com/ThemeQuest/specs-on-website
  1. Change directory
cd specs-on-website
  1. Install dependencies
npm i
  1. Run local development environment
npm run dev
  1. Done!
⚡ You're good to go

Usage

  • This project was developed specifically for stores like: Specsavers. After noticing that they don't have an online 'try it on' system for their specs/glasses. We decided to work on a system that will deliver instant feedback using Cloudinary for facial detection

For more information, please refer to the Cloudinary Docs

Roadmap

See the open issues for a list of proposed features (and known issues).

Contributing

Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License

Distributed under the MIT License. See LICENSE for more information.

Contact

LinkedIn - @StevenSelolo Twitter - @StevenSelolo GitHub - @StevenPss

LinkedIn - @rendani-alidzulwi Twitter - @blckink_za GitHub - @Rendani-Ally

Project Repo Link: https://github.com/ThemeQuest/specs-on-website

Acknowledgements

 Author: ThemeQuest
Source Code: https://github.com/ThemeQuest/specs-on-website
License: MIT License

#saas 

A Website That Makes Use Of The Specs API (glasses) Methods

絶対に使えるオススメのSaaSを紹介!

絶対に使えるオススメのSaaSを紹介!起業したい人やエンジニア向けのクラウドサービスです!

 

🚀 今日のひとこと
これ、撮影したはいいけどターゲット狭すぎて需要なさそう

📙 もくじ
0:39 G Suite
1:57 Discord (or Slack)
2:48 ClickUp (or Space)
4:19 Scrapbox
5:39 SmartHR
6:18 MoneyForward 
7:29 AI-CON 登記
7:56 シャショクラブ
9:10 GitHub
9:59 AWS (or GCP or Azure)
11:22 ZEIT Now
11:50 Auth0
12:43 Neo4j Aura
13:27 Algolia
13:44 PayPal
13:59 Doka
14:27 Imgix (or ImageFlux)

  #saas  

絶対に使えるオススメのSaaSを紹介!