0.1.8 • Published 3 years ago

retoolkit v0.1.8

Weekly downloads
-
License
ISC
Repository
-
Last release
3 years ago

retoolkit

A highly opinionated React and Redux toolkit-based implementation.

What do we gain from this implementation?

  • No "actions".
  • No "action creators".
  • Purely hook-based.
  • Consistent design.
  • Clear thunk implementation.
  • Only 2 types of reducer functions; setters and APIs.
  • Async operations with a state lifecycle around Redux.
  • Easily to implement, use, and configure.

Why does it need to be any harder than this?

It doesn't!!


Why Does This Library Even Exist

Just trying to make implementation and use of Redux and React easier.

Personally, I have written a LOT of Redux and React code over years. The one thing I really liked was the move to functional components and the support React has been receiving in that area. The one thing that never got any easier was Redux. Eventually, Redux introduced their own toolkit, but it did not (for me) solve the problem of "just using it". I wanted to have as little configuration as possible, declare my reducers, forget that actions and "action creators" even existed, and apply my store with no consideration for the interworkings because if I needed to know that I would try to work for them.

Then I created a utility like this, but it was just not a package, and had several of friends and co-workers tell me they wished it was this easy to begin with. So, after a couple years of having it around, copy & pasting it everywhere in my projects, and through friends and such -- I decided to make what I usually do for me into a package with the hopes that someone else has an easier life using these flux frameworks, more specifically Redux in React.

This tries to take what the awesome Redux team folks have done and simplify it as much as possible. However, as a result, I have made some decisions on how things should be implemented. The amazing Redux folks gave us flexibility and tools and I am taking the work of giants and trying to add a little something that is a bit more opinionated.

So, for this package, over the years, my plan is to over simplify the use and implementation of Redux and React. That's it.


Glossary

WIP - Links do NOT work yet...

  1. Quick Start
  2. Concepts
  3. API Calls
  4. Context Passthrough
  5. Exposing Reducers
  6. Customizing the Store

Quick Start

In this section we will quickly get a basic application setup to use retoolkit.

(Step 1) Create a Reducer


We need to import the createReducer function to tell retoolkit that this is a reducer we want to register once we import it into our store in the next step. There is NO requirement to export anything. The anatomy of how these reducer resolver functions are setup and used is explained in the Reducer Anatomy section.

src/reducers/app/reducer.js

import { createReducer } from "retoolkit";

// This function tells retoolkit that we need to register a reducer with this configuration.
createReducer({
    // This is the namespace reference for the reducer itself.
    selector: "app",

    // This is the state that the reducer will initially have.
    initialState: {
        varOne: null,
        varTwo: null,
    },

    // These are your reducer functions that update state in the reducer.
    reducers: {
        setVarOne: varOne => ({ varOne }),
        setVarTwo: varTwo => ({ varTwo })
    }
});

(Step 2) Creating a Store & Registering Reducers


Next, all we have to do is import everything we want to register before we create the store itself. In the case of this library, you can expect a React component back from the createStore function we use here. The returned React component automatically bootstraps the Redux provider into your application with the registered reducers. Registering a reducer doesn't require anything more than importing it.

src/store.js

// In this case, we only have one reducer. If we have more we would import them also.
import { createStore } from "retoolkit";

import "./reducers/app/reducer";
// import "<path>/<to>/<another>/<reducer>"; <-- If we had others to import.
// import "<path>/<to>/<another>/<reducer>"; <-- If we had others to import.

export const Store = createStore();

(Step 3) Apply Store & Use Reducers


Applying the "Store" is as simple as importing the "Store" component and wrapping our application with it. Using the reducers is as simple as loading them from retoolkit and invoking them just as normal hooks that return their respective parts of the reducer (either the reducer functions or the state).

src/index.jsx

import { Store } from "./store";
import { App } from "./components/App";

ReactDOM.render(
    <Store>
        <App />
    </Store>,
    document.querySelector("#app")
);

The only thing we need to be aware of is the "app" section of the code. That refers directly to the selector property from the reducer. That is how retookit can distinguish between all the reducers. Beyond that, it is just regular usage hooks. The first hook are the reducers themselves that you call like normal setter functions and the second hook is direct access to the data of that reducer.

src/components/App.jsx

import { loadReducers } from "retoolkit";
const [ useApp, useAppData ] = loadReducers("app");

export const App = () => {
    const { setVarOne } = useApp();
    const { varOne } = useAppData();

    useEffect(() => {
        if (!varOne)
            setVarOne(9999);
        if (varOne === 9999)
            console.log("MY AWESOME STUFF AND THINGS!!!");
    }, [ varOne ]);

    return (
        <>
            {/* My cool component stuff... */}
        </>
    );
}

Concepts

For this library there is a basic mental modal to consider. It is that all reducer states are Objects and all reducer functions return Objects with key and value pairs (properties) that update state when merged with the managed state.

All reducer functions are invoked the same way, regardless of the type of reducer function that it is. This is accomplished by making all the keys of the reducers Object configuration into functions that dispatch the necessary reducer function.

Beyond that, access to state can be accomplished either reducer-wide or per-reducer-function depending on your reducers needs.

Everything in state management per-reducer is done as follows:

// Think of this as current state.
let state = {
    varOne: null,
    varTwo: null,
    varThree: null,
    // Etc...
}

// Think of this as an update to that state.
const updates = {
    varOne: 1,
    varTwo: 2,
    varThree: 3,
    // Etc...
};

// Think of this as the operation of how state
// is updated in retoolkit.
//
// All updates are handled as merged objects,
// which is why you NEED to return an object
// from the reducer function. Technically, you
// can also "add" new properties as well.
//
// Anything you can do merging objects can be done.
state = {...state, ...updates };

API Calls

API calls can be done with retoolkit by making the reducer function into an Object with a specific set of properties that get called in a consistent and predictable manner. All API call reducers should have 4 properties; pending, resolve, success, and failure. Each property is a function. The only exception is the resolve function, which is an AsyncFunction type. In short, it uses async/await (Promises) to "resolve" the API call (also called a resolver function).

ALL of these functions are, effectively, reducer functions. Each one should follow the EXACT same pattern as the non-API functions. Return an Object that contains the key/value pairs of properties you want to update in the store.

The only catch here is that the resolve function receives all the arguments to the reducer function that gets called.

Thunk Lifecycle

Here is an example reducer with one reducer that is an API calling reducer.

import { createReducer } from "retoolkit";

createReducer({
    selector: "app",
    initialState: {
        // Here we want to be able to tell if the API
        // is loading, has been run at least one time,
        // and if there are any errors while trying to
        // run the API.
        varOne: null,
        isLoading: false,
        isChecked: false,
        isError: null,
    },
    reducer: {
        setVarOne: {
            // Gets called before the "resolve" function
            // and updates state.
            pending: () => ({ isLoading: true }),

            // Gets called to resolve the API call.
            resolve: async () => await apiCall(),

            // [IMPORTANT]
            // Whatever gets pass back from "resolve" gets
            // passed into "success" to be operated on.
            //
            // Get called AFTER the "resolve" function,
            // if nothing went wrong, and updates state.
            success: ({ data }) => ({
                isLoading: false,
                isChecked: true,
                setVarOne: data
            }),

            // Gets called if an exception was thrown
            // when trying to call "resolve", and updates state.
            failure: isError => ({
                isLoading: false,
                isChecked: true,
                isError
            }),
        }
    }
});

The call stack is like this (from top down):

  1. pending()
    • ALWAYS gets called.
  2. resolve(...args)
    • (IMPORTANT) Receives ALL argument data when reducer is invoked.
  3. success(...resolveArgs)
    • (IMPORTANT) Receives ALL data returned from the resolve function.
    • ONLY gets called if calling the API did NOT result in an exception.
  4. failure()
    • ONLY gets called if calling the API DID result in an exception.

Invoking API Reducers

Once you have it created, as shown above, invoking it is the EXACT SAME as the other reducers. Nothing is different. That is the key takeaway of this library. It does NOT require special invocations between types of calls. ALL reducer calls should be invoked the same exact way all the time.

import { loadReducers } from "retoolkit";
const [ useApp ] = loadReducers("app");

export const MyComp = () => {
    const { setVarOne } = useApp();

    useEffect(() => {
        // Invoked the reducer which WILL call
        // all the functions automatically.
        setVarOne();
    }, []);

    return // Whatever...
}

As far as types of reducers are concerned, that is it. There are only 2 types of functions. Those that are effectively cheap setters and those that are more sophisticated API calls. That's it.


Context Passthrough

The use of the context passthrough is to quickly, easily, and in a straight-forward way, pass some data through retoolkit that may or may not be needed in order places. The goal of this was to bridge the gap between hooks and non-hooks to be able to pass data wherever it is needed.

Why Does This Exist?

The problem here is how do you easily, quickly, and simply pass data from a component using hooks, context, etc, into a reducer without having to actually pass the data for that reducer into the reducer function itself? In short, how do you bridge the gap between hooks and non-hooks?

You can't use React context because you have to be within a hook or component to do that. You could create a wrapper functions that auto-injects the common data into your reducer, but then you could have to manage a lifecycle around the already managed reducer. All those ideas aren't bad, but they are not ideal.

So, why can't we have a common context that is accessed by a hook but also accessed functionally from within the reducer? We can. That is basically what this does, using 2 methods, respectively; useRetoolkitContext and loadContext;

Usage

You work for a company that uses a set of API metadata information. It usually does not change, but can based on which applications the business is using or environment it is deployed in. They want all your API calls to use this information. The API metadata can, with retoolkit be directly accessed via the hook or the standard function. The difference is that the hook gives you the familiarity and ability to leverage known standards that make development in React easy. The standard function gives you a link between the hook and the context.

In this scenario, I want to add an ID to the metadata to ensure that the user has the right ID for the application they are accessing. We need 2 things for this:

  • useRetoolkitContext (to expose it beyond).
  • loadContext (to use it outside the components in our reducer).

For this we will have 2 components. One component to set everything up and the other to call some API reducer that will access (consume) the data that we have configured in our context.

Configuration Component:

src/components/ConfigComponent.jsx

import { useState, useEffect } from "react";
import { useRetookitContext } from "retoolkit";

export const ConfigComponent = ({ children }) => {
    // Controls rendering of children until ready.
    const [ ready, setReady ] = useState(false);

    // Manages context data we need to have access to.
    const [ context, setContext ] = useRetoolkitContext();

    useEffect(() => {
        // If the data does not exist, add it.
        if (!context?.api)
            setContext({ api: {/* Our important data... */}});
        // If the data does exist, allow rendering.
        else
            setReady(true);
    }, [ varOne ]);

    // Only render when "ready".
    return ready && children;
}

Below this, we will look at just the reducer function of setVarTwo to get an idea of what that would look like. If you are confused about API calls, please see the section called API Calls.

API Component:

src/components/ApiComponent.jsx

import { loadReducers } from "retoolkit";
const [ useApp ] = loadReducers("app");

export const ApiComponent = () => {
    // In this example, "setVarTwo" is making an API call.
    const { setVarTwo } = useApp();

    useEffect(() => {
        setVarTwo();
    }, []);

    return (
        <>
            {/* Stuff and things... */}
        </>
    );
}

Here is a sample of how we might consume this context data outside a React component and use it in our reducer without needing to pass it into the reducer itself.

src/reducers/app/setVarTwo.js

import { loadContext } from "retoolkit";

export default {
    pending: () => ({/* Update state for pending... */}),

    // This is the part we care about. This is one way we could use this.
    // Directly in the resolver function of the API call. Personally, I would
    // NOT do this here. The best place for this is inside the API call code
    // itself. Which is shown in other examples.
    resolve: async () => {
        const { api } = loadContext();
        return await apiCall(api);
    }

    success: () => ({/* Update state for success... */}),
    failure: () => ({/* Update state for failure... */}),
}

This can be difficult to consider because it is not immediately clear why this is useful but I have encountered many situations where I wished data I was using was in some common area I could often access outside a component within a reducer or API file. This is meant to make that movement of data as easy as possible between components, hooks, reducers, and APIs.

If this is combined with React context, you could have a globally available state to React components without using retoolkit and only access the retoolkit context for the APIs by themselves. That might make the code more manageable at large scales.

WIP...

0.1.8

3 years ago

0.1.7

3 years ago

0.1.6

3 years ago

0.1.5

3 years ago

0.1.4

3 years ago

0.1.3

3 years ago

0.1.2

3 years ago

0.1.1

3 years ago

0.0.13

3 years ago

0.0.12

3 years ago

0.0.11

3 years ago

0.0.10

3 years ago

0.0.9

3 years ago

0.0.8

3 years ago

0.0.7

3 years ago

0.0.6

3 years ago

0.0.5

3 years ago

0.0.4

3 years ago

0.0.3

3 years ago

0.0.2

3 years ago