npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ryo-98/react-api-bridge

v0.2.1

Published

A registry for component imperative api

Downloads

4

Readme

react-api-bridge

react-api-bridge is aim to build a registry for imperative api in react based app. Once api is registered, it will be available anywhere in Function Component.



Install

  npm install @ryo-98/react-api-bridge

Simple Usage

    import createBridge from '@ryo-98/react-api-bridge';
        
    // create a bridge registry.
    const actionBridge = createBridge();
        
    function A()   {
        const [count, setCount] = useState(0);
        
        // register the api named 'aAPI' by 'useRegister' hook. the params of last two ones is same as 'useImperativeHandle'
        actionBridge.useRegister('aAPI', () => {
        
            // return the api
            return {
                getCount: () => count
            }
            
            // The third param is deps(optional).
        }, [count]);
        
         return <button onClick={() => setCount(count + 1)}>
            add
         </button>
    }
    
    function B() {
        // To get the api, use 'useAPI' hook.
        const aComponentAPI = actionBridge.useAPI('aAPI');
        
        return <button onClick={() => {
            console.log(aComponentAPI.current?.getCount());
        }}>show count of A </button>
    }
    
    function BWrapper() {
        return <><B/></>
    }
    
    function Surface() {
        return <><BWrapper/></>
    }
    
    
    function APP() {
        
        return <>
            <A/>
            {/*
             The B is within this component,
             but the registered api ref object of A is
             still avaliable to B.
             */}
            <Surface/>
            <B/>
        </>
    }   
        

useRegister and useAPI

The useRegister hook is to register the api for bridge it belong to, and then use the correspond useAPI hook to get the registered ref object of api .

// create Bridge in the top-level scope
import {useEffect} from "react";

const Bridge1 = createBridge();

// you can create multiple bridges to sparate namespaces for api.
const Bridge2 = createBridge();

function A() {
    const [name, setName] = useState('123');
    Bridge1.useRegister(
        'name',    //fist param is the name of the api, can regard as the id of the api
        () => { // second param is a function that should return the api 
            return {
                getName: () => name,
                setName: () => setName(name)
            }
        },
        [name] // third param is deps for updating the api
        // there has optional fourth param, will explain in a later section
    )

    // Technically you can get the api ref, in the same component context,
    // but most of time, it's worth nothing
    const nameAPI = Bridge.useAPI('name');

    //The useRegister is allow to use multiple times in the same component context.
    // register other api named 'pop'
    Bridge1.useRegister('pop', () => {
        return {
            // some perporties
        }
    });
    
    return //... some jsx
}

function B() {
    //There using useAPI hook to get the ref object of api named 'name' which was registered in the A component.
    const nameAPI = Bridge1.useAPI(
        'name',  
        // the second optional argument is an option object
        {
            // this method is able to listen the first completion of regsiter, 
            // and the regsitered api ref object is passed as its first argument,
        onInit(api) {
            console.log('name initialized',  api.current.getName());
            const handler =  () => {
                console.log('name: ', api.current.getName());
            };
            
            document.addEventListener('click',handler)
            
            //return a clear function if there has some effects need to be cleared in this function.
            return () => {
                document.removeEventListener('click', )
            }
        }
    });

    const handleClick = () => {
        // There, determine whether current is not undefined, 
        // because the api is not guarant have been registered
        if(nameAPI.current) {
            console.log(nameAPI.current.getName())
        }
    }
    
    return <button onClick={handleClick}>
        print name of A 
    </button>
}

After reading code above, there come a question:

Why need determine current value ?

From the code above,We know the register operation of 'name' api is only in the A component, and because A may not mount in the app, so whenever B is need to use 'name' api that the A component should register, it may just get undefined value from 'current' property due to the absence of the A component.

There have some points to note:

  • if a component's body have useRegister hooks and mount multiple times, the registered instance of api is the last mount one, because every time the useRegister is executed, it will overwrite the api in terms of name argument(the first parameter).

  • When api of useRegister updated due to the deps, the correspond useAPI hook will not cause component that it reside in re-render, because useRegister update the property 'current' value of the api ref object.

Boundary

The boundary is used to constrain communication among components and create a new registry to receive registered api, only components that in the same boundary context can do such. By default, if there is no boundary above a component in an element tree, it is in the global boundary implicitly.

    const Bridge1 = createBridge();
    
    function APP() {
        return <>
             <A/> // It is 'awarable' for component that in the global boundary.
             <Bridge1.Boundary>
                <B/> //the outer A is 'unawarable' for B, expect C and the inner one A , because C is in the same boundary with B
                <C/>
                <A/>
            </Bridge1.Boundary>
    </>
    }

Note different bridges are independent, their boundary is unable to perceive for others.

Associate with Boundaries or Hooks

Boundary can receive a 'contextValue' prop to associate with other boundaries or hooks.

import createBridge from "@ryo-98/react-api-bridge";

const Bridge1 = createBridge();

function APP() {
    const contextValue = Bridge1.useContextValue();
    Bridge1.useRegister(
        'root',
        () => {
            return {
                // some methods
            }
        },
        {
            // here pass the contextValue, boundary can associate with the hook, 
            // otherwise the api of 'root' will register in the upper boundary(In this case, it will be global boundary).
            contextValue
        });
    
    return <>
        <Bridge1.Boundary contextValue={contextValue}>
            <A/>
            <B/>
        </Bridge1.Boundary>
        
        {/* Associate with other boundary: */}
        {/* Pass the contextValue for  subsequent one that is the boundary of Bridge1  */}
        <Bridge1.Boundary contextValue={contextValue}>
            <C /> {/* C is as if in the same boundary as A and B. */}
        </Bridge1.Boundary>
    </>
}

Boundary Payload

Boundary can receive a prop named 'payload', it has same effect as the 'value' prop of Context.Provide, then the components within the Boundary can get the payload by specific hooks.

import createBridge from "@ryo-98/react-api-bridge";
// pass the default value of global boundary
const Bridge1 = createBridge("1");

function APP() {
    // Value is "1", because it within the global boundary.
    const payload = Bridge1.useBoundaryPayload();

    // create a bridge contextValue, and pass the value "2" as
    // payload value of associated boundaries.
    const contextValue = Bridge1.useContextValue("2");
    // Value is "2", because it is associated by passing a contextValue,
    // and the payload is "2".
    const payload2 = Bridge1.useBoundaryPayload({contextValue});
    
    return <>
        {/* Will print "1" */}
        <A/>
        
        <Bridge1.Boundary payload={"3"}>
        {/* Will print "3"   */}
            <A/>
        </Bridge1.Boundary>

        {/*
            if the payload and contextValue are both passed,
            the 'payload' prop will be ignore.
         */}
        <Bridge1.Boundary contextValue={contextValue}
                          payload={"3"}>
            {/*  Will print "2"  */}
            <A/>
        </Bridge1.Boundary>
    </>
}

function A() {
    const payload = Bridge1.useBoundaryPayload();
    console.log(payload);
    return <></>
}

useUpperAPI and useUpperBoundaryPayload

These hooks are able to traverse the boundaries above, and forward a specific boundary's api and payload respectively.

// the first argument is payload of global boundary.
const Bridge = createBridge('hello');

function A(props) {
    Bridge.useRegister('msg', () => {
        return {
            getMessage: () => props.message
        }
    }, [props.message]);
    return // some jsx
}

function B() {
    const nameAPI = Bridge.useAPI('msg');

    const wrapperNameAPI = Bridge.useUpperAPI('msg');

    const mainNameAPI = Bridge.useUpperAPI('msg', {
        shouldForwardYield(boundary) {
            return boundary.payload === 'main'
        }
    });

    const globalNameAPI = Bridge.useUpperAPI('msg', {
        shouldForwardYield(boundary) {
            return !boundary.parent;
        }
    });

    // will be 'hello'
    const globalPayload = Bridge.useUpperBoundaryPayload({
        shouldForwardYield(boundary) {
            return !boundary.parent;
        }
    })

    const handleClick = () => {
        // print 'baz'
        console.log(nameAPI.current?.getName())
        // print 'foo'
        console.log(wrapperNameAPI.current?.getName())
        // print 'bar'
        console.log(mainNameAPI.current?.getName())
        // print 'pop'
        console.log(globalNameAPI.current?.getName())
    }

    return <button onClick={handleClick}>
        get names
    </button>
}

// There is the hierarchy
function APP() {

    return <>
        <A message={'pop'}></A>
        <Bridge.Boundary payload={'main'}>
            <A message={'bar'}/>
            <Bridge.Boundary>
                <A message={'foo'}/>
                <Bridge.Boundary>
                    <A message={'baz'}/>
                    <B/>
                </Bridge.Boundary>
            </Bridge.Boundary>
        </Bridge.Boundary>
    </>
}

From the code above, the component B is using four hooks to get api named 'msg' in different boundary, the A component will receive a 'msg' prop, then register an api named 'msg'.

here will explain how each hook in the B component is working.

  1. useAPI, will look for an api named 'msg' in its boundary, and a component named 'A' with a prop valued 'baz' is under the same boundary with B, and A registered the api named 'msg' is just what B's useAPI is looking for.So the getMessage method of ref object returned by useAPI will return 'baz'.

  2. The useUpperAPI, is looking for an api named 'msg' on the one layer higher boundary of its current boundary, and on that layer boundary ,because a component named 'A' is register an api named 'msg', so the getMessage method of ref object returned by useUpperAPI will return 'foo'.

  3. The second useUpperAPI is passed an object type option argument, and that object have a method shouldForwardYield, and this method is to determine the boundary where the api should look for, the method hava a single argument which is a detail of current iterating boundary, and need return a value to determine if yield forward iteration, truly value means yield, otherwise keep going. the code snippet:

        return boundary.payload === 'main';

    means the api will look up in the boundary which have a payload valued 'main', it does have a boundary with this payload, and there have a component A register an api is what this useUpperAPI hook need. So the correspond getMessage method will return 'bar'.

  4. The third useUpperAPI also have a shouldForwardYield method on its option argument, but intend to look for an api in the boundary which have no parent, because only global boundary not have parent boundary itself, so in this case, it gets the api registered at most outer component A, and so the correspond getMessage method will return 'pop'.

Besides useAPI and useUpperAPI, the example also show a usage of the useUpperBoundaryPayload and its accept a same option object with useUpperAPI, it will get the payload of global boundary because its shouldForwardField is same as the last useUpperAPI used. Global payload can only be specified in the bridge creation, by passing first argument to createBridge.

Hook Counterparts

The hooks noticed above have their own normal version counterpart respectively:

    function A() {
    const {
        getAPI, // a counterpart for useAPI
        getBoundaryPayload, //  useBoundaryPayload
        getUpperAPI, // useUpperAPI
        getUpperBoundaryPayload, // useUpperBoundaryPayload
    } = Bridge.useTools();
    const api = Bridge.useAPI('someone');
    
    const show = () => {
        // the hook way
        console.log(api.current.getName())
        // here is the equivalent version
        console.log(getAPI('someone').current.getName());
    }
    
    return //some jsx
}

There also have a global version for useAPI,and not be required to use into function component.

const Bridge = createBridge();
// will get api named 'msg' from global boundary
const msgAPI = Bridge.getAPI('msg');
const thirdPartyBtn = document.querySelector('#mui-btn');

thirdPartyBtn.addEventListener('click', () => {
    if(msgAPI.current) {
        // will print 'baz'
        console.log(msgAPI.current());
    }
})

function A(props) {
    Bridge.useRegister('msg', () => {
        return () => props.message
    }, [props.message])
}

function APP() {
    
    return <>
        <A message={'baz'}/>
        <Bridge.Boundary>
            <A message={'bar'}/>
        </Bridge.Boundary>
        </>
}

Multiple API

The useRegister hook normally will only register singleton api instance, if there have been assigned value on the 'current' property of correspond ref object, it will overwrite with own value, this behaviour can switch to the new api will append to an array, and the array is containing api objects that have been registered.

The following code will show you how to switch this behaviour on:

import Bridge from "./bridge";

const Bridge = createBridge('pop')({ // invoke the function returned by createBridge, and pass an option object
    // it means api named 'msg' will use the 'mutilple' mode
    msg: {isMulti: true}
})

function A(props) {
    Bridge.useRegister('msg', () => {
        return {
            getMessage: () => props.message
        }
    }, [props.message]);

    Bridge.useRegister('evt', () => {
        return {
            show: () => console.log(props.evt)
        }
    }, [props.evt]);

    return // some jsx
}

function B() {
    Bridge.useRegister('msg', () => {
        return {
            getMessage: () => 'b-message'
        }
    });

}

function APP() {
    const msgAPIList = Bridge.useAPI('msg');
    const evtAPI = Bridge.useAPI('evt');
    
    const handle = () => {
        msgAPIList.forEach((api) => {
            // will print 'baz' 'bar' 'b-message'
            console.log(api.current.getMessage());
        })

        // only print 'c1'
        evtAPI.current.show();
    }

    return <>
        <A message={'baz'} evt={'r1'}/>
        <A message={'bar'} evt={'c1'}/>
        <B/>
        <button onClick={handle}>click me</button>
    </>
}

Troubleshooting

  • Why the api registered in a component not work even that component is mounting:
    • The component that registered the api may mount multiple times,but you may operate the last mounted instance of api, not the first one you may expect.

      <>
            <RegisterAPI /> 
           {/* will overwrite the previous api whenever showSecond is truly */}
           {
              showSecond && 
              <RegisterAPI /> 
           }
      </>
    • The component that registered the api, have a different boundary with the usage of api

      <>
         <SomeBridge.Boundary>
             <RegisterAPI /> 
         </SomeBridge.Boundary>
         {/* Will not able to get api that RegisterAPI component which is mounting above registered because their boundary is different */}
         <UsageOfAPI /> 
          
       </>

Typescript

The createBridge support two generic parameter.

  • The first generic parameter is the type for api, it should be given an object-like type as outer, but type of members are not constrained.
  • The second generic parameter is the payload type for global boundary, its optional, it will use undefined type by default.
// a common practice
const Bridge = createBridge<
    {
        msg: {
            getMessage: () => string,
        }
    },
    string
>('pop')();
// the member type of msg above is able to assigned any type.
const Bridge = createBridge<
    {
        msg: () => string
    }
    >()()


function A (props) {
    // then the register pattern will be this:
  Bridge.useRegister('msg', () => {
        return () => props.message
    }, [props.message])
    
    return //some jsx
}