State Plugin

With State plugin you can develop as a hero using redux patterns, also when using this package, all Flew's network calls are sped up by default using results stored in memory.

The state plugin is suited to work on client side only. It won't work on server.

Install

npm install @flew/core @flew/network @flew/state

For a better development experience, also make sure to install Chrome's Redux DevTools.

Configure

import { setup } from '@flew/core';
import { statePlugin } from '@flew/state';

setup(
  plugins: [
    statePlugin({
      production: false, // so we can use dev tools
      trace: true, // enable trace for dev tools
      traceLimit: 25, // configure trace limit
      reducers: { ... } // list of custom reducers
    }),
  ],
});

Network call

By default the state plugin acts with an internal reducer for network so we can store results in memory speeding up responses even more.

import { fetch } from '@flew/network';
import { lastValueFrom } from 'rxjs';

// create user
const newUser = await lastValueFrom(
  fetch('User').set({
    name: 'John',
  }),
).then(result => console.log(result));

// Output
// { name: "John", objectId: "a1b2c3" }

// update user
await lastValueFrom(
  fetch('User').doc(newUser.objectId).update({
    name: 'John Doe',
  }),
);

// get user for the very first time (it will be stored in memory)
fetch('User')
  .where('objectId', '==', 'a1b2c3')
  .findOne()
  .subscribe(
    user => console.log(user),
    err => console.log(err),
  );

// Stream outputs 1 result
// { name: "John Doe", objectId: "a1b2c3" }

// Update user again
await lastValueFrom(
  fetch('User').doc(newUser.objectId).update({
    name: 'John Snow',
  }),
);

// get user
fetch('User')
  .where('objectId', '==', 'a1b2c3')
  .findOne()
  .subscribe(
    user => console.log(user),
    err => console.log(err),
  );

// Now stream outputs 2 results in a row
// since we had 1 result stored in memory
// { name: "John Doe", objectId: "a1b2c3" }
// { name: "John Snow", objectId: "a1b2c3" }

Check DevTools to see results stored in the network state

Disable state in runtime

fetch('User')
  .where('objectId', '==', 'a1b2c3')
  .state(false) // <- this will disable state for this call
  .findOne()
  .subscribe(
    user => console.log(user),
    err => console.log(err),
  );

// Since state is disabled, the stream will
// always deliver a fresh result from network
// { name: "John Snow", objectId: "a1b2c3" }

Use arbitrary APIs

With Flew's state plugin you don't need to exclusively depend on the internal state behavior. You can also define custom state data.

Set a custom state

import { setState, getState } from '@flew/state'

fetch('User')
  .where('objectId', '==', 'a1b2c3')
  .state(false) // <- disable default state behavior
  .findOne()
  .subscribe(
    user => {
        setState('My-Custom-State', {
            myUser: user
        })

        const userFromState = getState('My-Custom-State');

        console.log(userFromState);
        // Output
        // { myUser: { name: "John Snow", objectId: "a1b2c3" }}
        }
    },
    err => console.log(err),
  );

Whenever you have the cachePlugin set up, setState will also store in browser's cache by default. You can disable this behavior by passing the cache option as third argument.

setState(
  'My-Custom-State',
  {
    myUser: user,
  },
  {
    cache: false,
  },
);

Clean up stored data

import { resetState, unsetState } from '@flew/state';

// Resets the whole state store
resetState();

// Clears state by a given key
unsetState('My-Custom-State');

Redux pattern

Flew's state plugin implements the core of Redux principles, so you can create reducers and actions out-of-box with its simplified apis.

Create reducers

With this example we're going to create a custom Session reducer

import { createReducer } from '@flew/state';

interface Session {
  token: string;
  user: any;
}

const session = createReducer<Session>(
  // initial values
  {
    user: {},
    token: null,
  },
  // reducers
  {
    sessionSetUser: (state, action) => {
        state.user = action.payload;
    },
    sessionSetToken: (state, action) => {
        state.token = action.payload
    },
  },
);

const navigation = createReducer<{
  lastPathname: string;
  currentPathname: string;
}>(
    // initial values
  {
    lastPathname: null,
    currentPathname: null
  },
    // reducers
  {

    navForward: (state, action) => {
      state.lastPathname = state.currentPathname;
      state.currentPathname = action.payload.route;
   },

    navBackward: (state, action) => {
      state.lastPathname = state.currentPathname;
      state.currentPathname = action.payload.route;
    },
  }
);

// enable custom reducers on app startup
import { setup } from '@flew/core';
import { statePlugin } from '@flew/state';

setup(
  plugins: [
    statePlugin({
      // ...
      reducers: { session, navigation }
    }),
  ],
});

Create sync actions

Synchronous actions are actions that receives a flat payload and don't do any async operation.

import { createAction } from '@flew/state';

const sessionSetToken = createAction<string>('sessionSetToken');
const sessionSetUser = createAction<any>('sessionSetUser');

Create async actions

Asynchronous actions are actions that receives a payload, do some time consuming operation, and then return its type and payload in order to tell the reducer to modify its state. The example below is a navigation action which takes a route as a parameter and call a network side effect.

// define the action
function navForward(route: string) {
  return async function (dispatch) {
    const payload = {
      type: 'navForward',
      payload: {
        route,
      },
    };

    await updateSessionOnlineAt(); // side effect

    // tell the linked reducer to change its state
    dispatch(payload);

    // navigate to the given route
    window.location.pathname = route;
  };
}

Note that in this case, the dispatch method isn't imported by Flew, but injected as a parameter to the returning function.

Use actions

The goal of redux actions is to tell the linked reducer to modify its state, in this case we always need to use Flew's dispatch method combined with the action in question.

import { dispatch, getState } from '@flew/state';

// set user's token
dispatch(sessionSetToken('bf1be4e06853'));

// get user's token
const userToken = getState('session.token');

console.log(userToken); // bf1be4e06853

// navigate to users route
dispatch(navForward('/users'));

Reactive state

With the connect api you can link a specific piece of state directly into the view since it returns an observable.

Example in Angular

import { fetch } from '@flew/network';
import { connect } from '@flew/state';

@Component({
  selector: 'my-app',
  template: ` Hello {{ (user$ | async)?.name }} `,
})
export class AppComponent {
  user$ = connect<any>('session.user');

  ngOnInit() {
    fetch('User')
      .where('objectId', '==', 'a1b2c3')
      .findOne()
      .subscribe(user => dispatch(sessionSetUser(user)));
  }
}

Example using internal Flew's network state

import { fetch } from '@flew/network';
import { connect } from '@flew/state';

@Component({
  selector: 'my-app',
  template: ` Hello {{ (user$ | async)?.name }} `,
})
export class AppComponent {
  user$ = connect<User>('myUser', { network: true });

  ngOnInit() {
    fetch('User')
      .key('myUser') // set a custom key to link with
      .where('objectId', '==', 'a1b2c3')
      .findOne();
  }
}

Since fetch api always returns an observable, we can also tie it directly to the view.

import { fetch } from '@flew/network';
import { connect } from '@flew/state';

@Component({
  selector: 'my-app',
  template: ` Hello {{ (user$ | async)?.name }} `,
})
export class AppComponent {
  user$ = fetch('User') //
    .where('objectId', '==', 'a1b2c3') //
    .findOne();
}

Although Flew gives us all those flexibility, we recommend to always be using Redux patterns for easy code debugging and readability.

Connect context

Since every redux reducer isn't actually mutating its state, the connect api also provides a way to easily build features that needs to know what was the previous state.

Example

import { connect, dispatch } from '@flew/state';

connect('session.user', {
  context: true, // <- Enables state context
}).subscribe(({ prev, next }) => {
  console.log('previous user', prev);
  console.log('next user', next);
});

dispatch(
  sessionSetUser({
    name: 'John Doe',
    objectId: 'a1b2c3',
  }),
);

// output
// previous user {}
// next user  { name: 'John Doe', objectId: 'a1b2c3' }

dispatch(
  sessionSetUser({
    name: 'Daenerys Targaryen',
    objectId: 'e5f6g7',
  }),
);

// output
// previous user { name: 'John Doe', objectId: 'a1b2c3' }
// next user { name: 'Daenerys Targaryen', objectId: 'e5f6g7' }
Edit this page on GitHub Updated at Fri, Sep 16, 2022