react-miniverse

Simple rxjs state manager

Usage no npm install needed!

<script type="module">
  import reactMiniverse from 'https://cdn.skypack.dev/react-miniverse';
</script>

README

react-miniverse

What is react-miniverse? It is a simple state manager. It can be used to fetch data, cache the data and update all components that are subscribed to the resource.

You can also use react-miniverse to share the state between components.

Works great with nextjs share state between the server and client.

1.8K minified or only 795bytes gzipped.

Why did you create this?

After setting up Nextjs with redux and redux-saga i was really not looking forward creating the stores and sagas. Most of the time a state manager is overkill for just fetching and displaying a resource and forget all about it. The work in setting up all those stores over and over again.

I wanted something simple as in "Hey give me that data" and not having to think about the state of said data.

  • Not loaded? I will load it for you and will keep updated with new data. This is very useful for remembering and sharing login state.
  • Is loaded? I will give you what i got and will keep you updated with new data.
  • Share state between Server and Client? No problem.

Installation

npm npm install react-miniverse

yarn yarn add react-miniverse

nextjs

Take a look at the example folder. The example contains loading clientside only and loading of data trough the server or client.

Create an AppContextProvider

Inject the services into our application. We will use context to make the services available throughout the site.

Create a file called AppContext.tsx. This file contains a simple setup to create the context used throughout the site.

This example contains the interface called AppContextInterface. This is wrong try to be better.

You probably should create a services class containing some code from _app.tsx and a servicesInterface.

import * as React from 'react';
import {ServicesInterface} from "../services/services";


const AppContext = React.createContext<any>({});

export function withAppContext<T = ServicesInterface>(Component: React.ComponentType<T>) {
    return (props: any) => {
        return (<AppContext.Consumer>{(context) => (<Component context={context} {...props} />)}</AppContext.Consumer>);
    };
}

export const AppProvider = AppContext.Provider;
export const AppConsumer = AppContext.Consumer;
export default AppContext;

Create your service.

A service will talk to the backend and returns the response for you to use or to provide it to the StoreService The included api service will return ONLY the response.data and not the whole response object.

src/services/placeholder.service

import ApiService from "./api.service";
import {defer, from, Observable} from "rxjs";

export default class PlaceholderService {

    private static instance: PlaceholderService;

    private api: ApiService;

    public constructor(api?: ApiService) {
        if (!api) {
            this.api = ApiService.getInstance();

            return;
        }

        this.api = api;
    }

    public static getInstance(): PlaceholderService {
        if (typeof window === 'undefined') {
            return new PlaceholderService();
        }

        if (!PlaceholderService.instance) {
            PlaceholderService.instance = new PlaceholderService();
        }

        return PlaceholderService.instance;
    }

    public getUsers(options?: any): Observable<any> {
        return this.api.store.fetch(
            'api',
            'entries',
            defer(() => from(this.api.get('https://jsonplaceholder.typicode.com/users'))),
            options
        );
    }

    public getPosts(options?: any): Observable<any> {
        return this.api.store.fetch(
            'placeholder',
            'posts',
            defer(() => from(this.api.get('https://jsonplaceholder.typicode.com/posts'))),
            options
        );
    }

    public getCatFact(options?: any): Observable<any> {
        return this.api.store.fetch(
            'placeholder',
            'cat-fact',
            defer(() => from(this.api.get('https://catfact.ninja/fact'))),
            options
        );
    }
}

Create api.service.ts

Here we bring our favorite http client and miniverse together

import axios, {Axios, AxiosRequestConfig, AxiosResponse} from "axios";
import {Miniverse} from "react-miniverse";

export default class ApiService {

    private static instance?: ApiService;
    private http: Axios;

    public store: Miniverse;

    public constructor(store?: Miniverse) {
        if (!store) {
            this.store = Miniverse.getInstance();
        } else {
            this.store = store;
        }

        this.http = axios;
    }

    public static getInstance(): ApiService {
        if (typeof window === 'undefined') {
            // Serverside.
            return new ApiService();
        }

        if (!ApiService.instance) {
            ApiService.instance = new ApiService();
        }

        return ApiService.instance as ApiService;
    }

    public static closeInstance() {
        delete ApiService.instance;
    }

    public get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R> {
        return this.http.get<T, R, D>(url, config);
    }

    public addRequestInterceptor(callback: (config: AxiosRequestConfig) => AxiosRequestConfig | Promise<any>) {
        this.http.interceptors.request.use(callback)
    }
}

Create services.ts

This file will initialize all services

import ApiService from "./api.service";
import PlaceholderService from "./placeholder.service";

export interface ServicesInterface {
    api: ApiService,
    placeholder: PlaceholderService,
}

const services = {
    api: ApiService.getInstance(),
    placeholder: PlaceholderService.getInstance(),
}

export default services;

Setting up _app.tsx

Open up ./src/pages/_app.tsx. Here we will construct the services export data from the store and import it for the client re-hydration.

import '../styles/globals.css'
import {Miniverse} from "react-miniverse";
import PlaceholderService from "../services/placeholder.service";
import React from 'react';
import {AppProvider} from "../Components/AppContext";
import {NextPageContext} from "next";
import App, {AppContext} from "next/app";
import services from "../services/services";
import ApiService from "../services/api.service";

function MyApp({Component, pageProps, _store}: any) {

    // Rehydrate when running on the client.
    if (typeof window !== 'undefined') {
        Miniverse.getInstance().hydrate(_store);
    }

    return (
        <AppProvider value={services}>
            <Component {...pageProps} />
        </AppProvider>
    );
}

export declare type CustomPageContext = NextPageContext & {
    store: Miniverse;
    api: ApiService;
    placeholder: PlaceholderService;
};

export declare type CustomAppContext = AppContext & {
    ctx: CustomPageContext;
}

MyApp.getInitialProps = async ({Component, ctx}: CustomAppContext) => {
    const miniverse = Miniverse.getInstance();
    
    // Because state is saved when running on the server, it is necessary to create fresh clients every request.
    // Also here we could pass the request into the apiService to read cookies and alter the headers
    ctx.store = miniverse;
    ctx.api = new ApiService(miniverse);
    ctx.placeholder = new PlaceholderService(ctx.api);

    const pageProps = await App.getInitialProps({Component, ctx} as CustomAppContext)
    const _store = miniverse.export();
    miniverse.close();

    return {_store, pageProps};
};

export default MyApp

index.tsx

Open up your index.tsx or any other page and start loading some data.

import Head from 'next/head'
import styles from '../styles/Home.module.css'
import {useContext, useState} from "react";
import AppContext from "../Components/AppContext";
import {ServicesInterface} from "../services/services";
import {useMiniverse} from "react-miniverse";
import {CustomPageContext} from "./_app";
import {firstValueFrom} from "rxjs";

interface HomeInterface {
  pageProps: {
    posts: Array<any>;
    cat: any;
  };
}

function Home({pageProps: {posts, cat}}: HomeInterface) {

  const [params, updateParams] = useState<any>();
  const {placeholder} = useContext<ServicesInterface>(AppContext);

  /**
   * Subscribe to the observable that contains the users.
   *
   * Return the cold value if that value is undefined load the resource
   * If the cold value is already set return the value and subscribe for changes
   *
   */
  const githubUsers = useMiniverse(placeholder.getUsers())

  /**
   * Subscribe to the observable that contains the posts.
   *
   * Return the cold value and subscribe for any changes
   *
   */
  const hotPosts = useMiniverse(placeholder.getPosts(), posts)

  /**
   * Subscribe to the observable that contains the cat facts.
   *
   * Return the cold value and subscribe for any changes
   *
   */
  const hotCat = useMiniverse(placeholder.getCatFact({params}), cat);

  const reload = () => {
    placeholder.getCatFact({refresh: true});
  }

  const reloadWithParams = () => {
    updateParams({page: 2});
  }

  return (
          <div className={styles.container}>
            <Head>
              <title>Create Next App</title>
              <link rel="icon" href="/favicon.ico"/>
            </Head>

            <main className={styles.main}>
              <h1 className={styles.title}>
                Welcome to
                <a href="https://github.com/lemoncms/react-miniverse">ReactMiniverse.js!</a>
              </h1>

              <div>
                <h2>{hotCat?.fact}</h2>
                <button onClick={reload}>Reload cat fact</button>
                <br/>
                <button onClick={reloadWithParams}>Reload cat with params check</button>
              </div>

              <div>
                <h2>Load Client side only:</h2>
                <table>
                  <thead>
                  <tr>
                    <th>Username</th>
                  </tr>
                  </thead>
                  <tbody>
                  {(() => (githubUsers || []).map((user: any) => (
                          <tr key={user.id}>
                            <td>{user.name}</td>
                          </tr>
                  )))()}
                  </tbody>
                </table>
              </div>

              <div>
                <h2>Load on client and server:</h2>
                <table>
                  <thead>
                  <tr>
                    <th>Title</th>
                  </tr>
                  </thead>
                  <tbody>
                  {(() => (hotPosts || []).map((post: any) => (
                          <tr key={post.id}>
                            <td>{post.title}</td>
                          </tr>
                  )))()}
                  </tbody>
                </table>
              </div>
            </main>
          </div>
  )
}

Home.getInitialProps = async ({placeholder}: CustomPageContext) => {

  const posts = await firstValueFrom(placeholder.getPosts());
  const cat = await firstValueFrom(placeholder.getCatFact());

  return {posts, cat};
};

export default Home;


Done

You are all setup. As you can see it is easy to retrieve data and stay up to date. There is much more to it so please take a look at the sourcecode.

Contributing

Please do, merge request are welcome.

  • Update Readme add APi docs
  • Create tests

Happy coding!