0.0.12 • Published 4 years ago

react-tacklebox-ts v0.0.12

Weekly downloads
1
License
MIT
Repository
github
Last release
4 years ago

Disclaimer:

This library is still very much under development and isn't recommended for use (yet!)

Summary:

Tacklebox is an open source library to deal with a problem we ran into at Sonar: There's no lightweight way to creat global state in react applications. Tacklebox is a lightweight way to create global state in react applications. But it also does more, because as we were diving into it, we realized it's also a handy place to put global logic (for example, if you want to automatically make an API query every time a particular piece of global state changes).

So the first part of this README will have examples and guides for how to make global state using Tacklebox. Later on there'll be examples on how to do more complicated stuff using "services" -- but if you just want to use it for state, that's cool too :)

Notes:

Tacklebox is very much an in house project that we're making open source to see who else might be interested. As such, there are a lot of our design considerations that affect how it's built:

  • Typescript -- We use (and love) Typescript at Sonar. So Tacklebox and all its example are written in Typescript.
  • Hooks -- We use react functional components almost exclusively at Sonar, so we use a lot of hooks. Tacklebox exports all of its exposed services as hooks. There's nothing preventing us from exporting them as HoCs though, which would work with class-based components. If this is important to you, drop us a feature request and we'll see what we can do.
    • (The name "Tacklebox" comes from a joke I made one day that "it's where we keep all the hooks". You don't have to message me saying how bad this joke is -- I already know)
  • Context -- Tacklebox under the hood uses React's context api. There's no reason you couldn't write your own state and/or services using context directly. Tacklebox is just (we think) a handy way of organizing all these contexts. We'll assume you're familiar with contexts, but if you're not -- the basic idea is that by putting a wrapping <Provider> {around the rest of your app} </Provider> any element inside the <Provider> tags (no matter how deep in the tree) can access the stuff that is provided.

Getting Started:

Installation should be as easy as npm install --save-dev react-tacklebox-ts, so lets dive into how to actually start using Tacklebox from your codebase


When setting up a React app to using Tacklebox, somewhere at the very top of your application, put some code that looks like the following:

import { compileServices } from "react-tacklebox-ts";

// The compile is basically figuring out which context providers to set
// up in which order.  It doesn't matter if all you're doing is state,
// but it matters if you start using tacklebox for more complex logic

// The compile does have to run before your react app can run for the
// first time but don't worry it is relatively light and doesn't
// actually RUN any of your services, just sets up the ordering.
const serviceList = compileServices({
    exposedServices: {
        useHelloWorldState: composeService.globalState("World"),
        // 
        // stuff will go here eventually
    },
});
const {
    useHelloWorldState,
} = serviceList.hooks;

And then (this is critical) make sure you insert a <serviceList.ProvideAll> tag surrounding your entire app. It doesn't have to be RIGHT at the top, but if you try to call any Tacklebox services from outside the ProvideAll tag it'll throw a big ugly error for you. Something like:

const App = () => {
    return 
        <serviceList.ProvideAll>
            // rest of the application
        </serviceList.ProvideAll>
    ;
}

And then anywhere yould like to use global state, you can do:

function SampleComponent(props) {
    // local state version for comparison:
    // const [state, setState] = React.useState("default value");

    // global state version (this custom hook was generated
    // by Tacklebox during the compile):
    const {state, setState} = useHelloWorldState();
    // notice that the [] brackets are now {}.  Also notice the
    // default value is no longer present (since it is now
    // specified globally) -- but otherwise, the tacklebox
    // provided state hook works exactly how you think it would

    // ... rest of render function ...
}

At this point you can add more state hooks to maanage whatever state you need to:

const serviceList = compileServices({
    exposedServices: {
        // some weird examples, but hopefully gives you ideas:
        useGlobalUsername: composeService.globalState<string | undefined>(undefined),
        useGlobalFeatureFlagForFancyNewFeature: composeService.globalState(false),
        useGlobalFriendsList: composeService.globalState<Array<{username: string, id: number}>>([]),
    },
});
const {
    useHelloWorldState,
    useGlobalFeatureFlagForFancyNewFeature,
    useGlobalFriendsList,
} = serviceList.hooks;
// and the three generated hooks will have proper typing and everything.

One final note: it should be noted that the three state hooks in this example are fully independent. If you write a component that uses one of them, that component will be re-rendered whenever that state changes, but NOT if either of the other two change. Obviously you can write a component that uses 2/3 or even all 3 and then it does re-render after any state change, but that's up to you. You can make your state objects as coarse or as granular as you need. And if you just want to use tacklebox for global state, you can stop reading here. That's all there is to it.

Advanced Services

The readme isn't written yet for advanced services (stuff that goes beyond state and into functionality). I'll drop one example at the end here, but there's a lot more that needs to be written before this guide is complete. Before we get into that though let's talk about WHY you might like to have services, and what they can do for you. The basic philosophy is that IMHO React is very good at bundling UI components with the logic and services needed to make them work. Their Counter example on React's docs is a great example: it has a number display, a "+" and "-" button, and it manages itself. Self contained and clean -- I wish all code was this way. But sometimes it isn't and you need to have logic that's specified at a page wide level, and isn't tied to any particular DOM element.

Let's have an example. I'm going to gloss over a lot of details here, this is more just a high level idea of the kinds of things you could do. For example, let's say you have a JWT token stored locally to represent the user logged in, and there's a UserInfo API query that you should hit to get information about the current user. That information might include a bunch of different thing -- the user first name (which is displayed in the top right), the user's available teams (which is in the top left), and the users current settings (some of which are at the bottom).

So we have a global state for JWT token:

// In a real application this would be read from local storage
//  on application load, but that's not what we're focusing on here
const jwtService = composeService.globalState<string | undefined>(undefined);

And then an API method to query the user info

type UserInfo = {
    // ...
}
async function getUserInfo(authToken: string): Promise<UserInfo> {
    // .. could potentially throw errors if the API is unreachable or whatever
}

Then we can compose these two into new Tacklebox service, like so:

// delayedUpdate is a standard hook that has:
//  - dependencies
//  - an (aync) compute function
//  - a cached result
//  - a single global manager that will call the compute
//          function again if the dependencies trigger an update
const userInfoService = composeService.delayedUpdate({
    // list of services that this new service depends on
    // -- in our case, just one:
    jwt: jwtService,
}, deps => {
    return () => {
        // deps.jwt is defined here (with the proper type)
        // becase 'jwt' in the keys of the dependency hash, above
        if (!deps.jwt.state) { // this is why we have the convention of
                               // doing {state, setState} instead of
                               // [state, setState] -- makes it much
                               // more convinient here than deps.jwt[0]
            throw new Error("Not logged in");
        } else {
            getUserInfo(deps.jwt.state);
        }
}

Tacklebox makes you specify explicitly when your service has dependencies on other services. Because of this, it can make sure that the generated hooks follow the rules of hooks and also make sure that the context Providers at the root of your application (remember ProvideAll?) are nested in the proper order. When you compile your services:

const serviceList = compileServices({
    exposedServices: {
        useJWT: jwtService, // since this is a dependency of userInfoService below
                            // it gets compiled in whether you specifically ask for
                            // it or not -- BUT by putting a name on it we can get
                            // the hook 5 lines down.
        useUserInfo: userInfoService,
    },
});
export const { // usually exported from near the top of your app so components can use it
    useJWT,
    useUserInfo,
} = serviceList.hooks;

Your login component has free access to the JWT state, so after obtaining a new JWT token it's easy to do a setState call to set that. And it's also pretty easy to have a logout button that just does useJWT() ... setState(undefined) and whenever that state updates, your delayed update service will kick off a new API request (if applicable) and update the global UserInfo state. To put it a different way, any components scattered across your app reading the current user via useUserInfo will automatically update whenever the user info updates -- which updates after the auth token updates. This method ensures that the API method is called whenever the auth token is set (to a non-undefined), but is only called once globally, even if the result is being waited on by multiple components on screen.

0.0.12

4 years ago

0.0.11

4 years ago

0.0.10

4 years ago

0.0.9

4 years ago

0.0.8

4 years ago

0.0.7

4 years ago

0.0.6

4 years ago

0.0.3

4 years ago

0.0.2

4 years ago

0.0.4

4 years ago

0.0.1

4 years ago