Learning how to manage routes in React isn’t exactly the same… And incomplete instances run in vite + React-ts

Third party packages used

  • History Route Management
  • Path-to-regexp converts a path to a regular

History and Path-to-regexp are briefly introduced

history

History creates route management based on history Hash Memory

import {createBrowserHistory, createMemoryHistory, createHashHistory} from "history"
Copy the code

The return types are custom History interface types

export interface History<S extends State = State> {
    readonly action: Action; // The current operation type is PUSH POP
    readonly location: Location<S>;// Routing information of the current page
    createHref(to: To): string; // Create a jump linkpush(to: To, state? : S):void;// Add a recordreplace(to: To, state? : S):void;/ / replace
    go(delta: number) :void;// ...
    back(): void; // ...
    forward(): void; // ...
    listen(listener: Listener<S>): () = > void;// Enable listening for route changes
    block(blocker: Blocker<S>): () = > void; / /... Back to say again
}
Copy the code

path-to-regexp

The path can be converted to the corresponding re…

import { pathToRegexp, parse, compile, match } from "path-to-regexp";
Copy the code
  • PathToRegexp converts a path to a regular

  • Match Matches a path through a regular match

  • Which two don’t work?

const options = {end:false} // end matches the end
const regexp = pathToRegexp("/article/:id", [],options);

const exec = match(regexp)
exec("/article/1") // {xxx,params:{id:1}}
Copy the code

Understand the react – the router

Component tree structure

<Router>
    <Route path="/login/add">
        <LoginPage />
    </Route>
    <Route path="/login">
        <LoginPage />
    </Route>
    <Route path="/register/add" exact>
        <RegPage />
    </Route>
    <Route path="/register">
        <RegPage />
    </Route>
    <Route path="/add" exact>
        <DefaultLayout />
    </Route>
</Router>
Copy the code

When we use the code above, the component structure looks like this

The Router props exists. The location data exists in the history component

  • Location Stores information about the current route
  • History is passed in by Browser (in fact, the history object is derived from the history package above)

A context router. Provider is created on the router in the following format

  • History is also the current global routing object
  • Location stores the routing information of the current page
  • Match is the result of the current page based on the configuration (…).

Looking further down, this context is used within the Route component

The Router component creates a History Route manager and puts the location object of the current information on the history page of the operation in the context. The Route component uses the context and accepts our configuration information inside the Route component (props). For example, path exact component children and other information in the Route component to determine whether the page is displayed…

implementation

A simple implementation of Browser Router Route

  • Create folder router
  • Create the file browser-router.tsx route.tsx
  • Create the context router-context.ts

So let’s implement the context

Create types and initial values based on the react-router data interface

import React from "react";
import { History, Location } from "history";
import { MatchResult } from "./types";

// context is used after the initial value
export const RouterContextDefaultVal: RouterContextValueProps = {
    history: null.location: null.match: {
        isExact: true.params: {},
        path: "/".url: "/",}};// Create a context object
const Context = React.createContext<RouterContextValueProps>(
    RouterContextDefaultVal
);
Context.displayName = "Router";

// Context stores the value type
export type RouterContextValueProps = {
    history: History | null;
    location: Location | null;
    match: MatchResult;
};
export default Context;

Copy the code

router

Introduce what is needed… ??

import React, { useEffect, useMemo, useState } from "react";
import { History, Location } from "history"; // interface
import RouterContext, {
    RouterContextDefaultVal,
    RouterContextValueProps,
} from "./router-context";

Copy the code

Creating a Router Component

export type RouterProps = {
    history: History; 
};

export const Router: React.FC<RouterProps> = (props) = > {
    const { history, children } = props; // history The object created for the history package
    const [location, setLocation] = useState<Location | null> (null);// Current page (routing) information

    const value = useMemo<RouterContextValueProps>(() = > {
        // First render
        if (location == null) {
            return RouterContextDefaultVal;
        }
        // A new object is returned after a route change triggers child component rendering
        return {
            history,
            match: RouterContextDefaultVal.match, // This match is not needed here
            location,
        };
    }, [location]); // Reassign when the route jumps to trigger history.listen

    useEffect(() = > {
        // The first render initializes the information going into the route
        setLocation(history.location);
        const unListen = history.listen(({ action, location: newLocation }) = > {
            // Reset your location when the route changes and change the RouterContext Value (useMemo)
            The value useMemo dependency has been changed and the new RouterContext value is recalculated
            // Child components that depend on RouterContext will re-render...
            setLocation(newLocation);
        });
        return () = > unListen(); Cancel listening theory says that the page will not be uninstalled.
    }, [history]);

    return({// Pass value into the context object}
        <RouterContext.Provider value={value}>
            {children}
        </RouterContext.Provider>
    );
};
Copy the code

Route

Introduce the required dependencies

import React, { useContext } from "react";
import RouterContext from "./router-context";
import { History } from "history"; // interface
import { computedRouteIsRender } from "./util"; // Calculate the matching result
import { MatchResult } from "./types";// Match the result type
Copy the code
// ./util.ts
import { match, pathToRegexp } from "path-to-regexp";
import {
    RouterContextDefaultVal,
    RouterContextValueProps,
} from "./router-context";
import { MatchResult } from "./types";
/** * The path matches */
export const computedRouteIsRender = (
    path: string, // The path to be matched
    exact: boolean | undefined.// Exact match
    value: RouterContextValueProps // The context object uses its location for matching) :MatchResult= > {
    // When true the regexp will match to the end of the string. (default: true)
    // Matches the end of the string if end is true
    const regExp = pathToRegexp(path, [], { end:!!!!! exact });const exec = match(regExp, {
        decode: decodeURIComponent});constresult = exec(value.location? .pathname ||"");
    IsExact returns false if false fails to match
    if(! result) {return {
            ...RouterContextDefaultVal.match,
            isExact: false}; }// The match is successful
    return {
        isExact: true.path: path,
        url: value.location? .pathname ||"".params: result.params,
    };
};

Copy the code
// ./types.ts
export type MatchResult<P extends Object= {} > = {isExact: boolean;
    params: P;
    path: string;
    url: string;
};
Copy the code

The route to realize

The implementation of Route is simple. Because you have received the information from props, use this information to match the current path with the path and other information in the information, and select whether to display the page component

/** * children > component > render */
export type RouteProps = {
    path: string; // Match the pathexact? : boolean;// Whether the match is accuraterender? :(history: History | null) = > React.ReactNode; / / render functioncomponent? : React.ReactNode;/ / componentcomputedMath? : MatchResult;// use it after
};
export const Route:React.FC<RouteProps> = (props) = > {
    const { path, exact, render, children, component, computedMath } = props;
    const ctx = useContext(RouterContext);

    // Determine whether rendering is required
    // If computedMath exists, then switch is used instead
    // Failed to match
    const result = computedRouteIsRender(path, exact, ctx);
    console.log(computedMath);
    if(! result.isExact) {return <>{null}</>;
    }
    // The matching success is displayed according to the priority
    const MatchCom: React.ReactNode | null = children
        ? children
        : component
        ? component
        : render
        ? render(ctx.history)
        : null;

    // RouterContext.Provider is used here because child components might need to get a match. Match parameters and so on will be used later
    return (
        <RouterContext.Provider
            value={{
                . ctx.match: result,}} >
            {MatchCom}
        </RouterContext.Provider>
    );
};


Copy the code

Browser-router

The implementation here is easier, creating a history object. Just pass it into the Router component. Change to createHashHistory when using hash…

Create a browser – the router. The TSX

import { Router } from "./router";
import React, { useState } from "react";
import { createBrowserHistory, History } from "history";

export const BrowserRouter: React.FC = (props) = > {
    const [history, setHistory] = useState<History>(() = > {
        return createBrowserHistory({
            window}); });console.log(props);
    return <Router history={history}>{props.children}</Router>;
};

Copy the code

At this point, the test page into the path to their own implementation

// import {
// BrowserRouter as Router,
// Route,
// Redirect,
// Switch,
// } from "react-router-dom";
import { Route, BrowserRouter as Router } from "./react-router/index";
Copy the code

View the page component tree, it can be used normally

Switch

The React-router stops when the route of a child component matches

Let’s start with a piece of code and its output

const A = (props) = > {
  console.log(props.children)
  return <div>sb</div>
}

const C = (props) = > {
  return <div>nt</div>
}

const B = (props) = > {
  return <A>
   	<C count={1} />
    <C count={2} />
    <C count={3} />
  </A>
}

ReactDOM.render(<B />.document.getElementById("root"));

Copy the code

Get print results

[{$$typeof: Symbol(react.element),
    key:null.props: {count:1},
    xxx
  },
  {xxx},
  {xxx}
]
Copy the code

You can get the props of the child component by accessing props

That’s easy to do

import React, { useContext } from "react";
import RouterContext from "./router-context";
import { computedRouteIsRender } from "./util";

export const Switch: React.FC = (props) = > {
    const value = useContext(RouterContext);
    // Iterate over the child elements
    if (Array.isArray(props.children)) {
        for (let i = 0; i < props.children.length; i++) {
            const child = props.children[i];
            const {
                path,
                exact,
            }: { path: string; exact: boolean } = (child as JSX.Element).props;
            const result = computedRouteIsRender(path, exact, value);
           // A successful match returns the component directly and does not continue
            if (result.isExact) {
                return <>{child}</>; }}}return <>{null}</>;
};

Copy the code

But here’s the problem. How w do I inform the Route component that it no longer executes the computedRouteIsRender function? .

The rest, like Link useHistory useParams, are easier to implement

Link

import React, { useContext } from "react";
import RouterContext from "./router-context";
import { To } from "history";

export type LinkProps = {
    to: To;
};
export const Link: React.FC<LinkProps> = (props) = > {
    const ctx = useContext(RouterContext);
    return (
        <a
            href="#"
            onClick={()= >{ ctx.history? .push(props.to); }} > {props.children}</a>
    );
};

Copy the code

usexxx

import React, { useContext } from "react";
import RouterContext from "./router-context";

export const useHistory = () = > {
    const ctx = useContext(RouterContext);
    return ctx.history;
};


export const useParams = (): Record<string, string> => {
    const ctx = useContext(RouterContext);
    return ctx.match.params;
};

export const useState = () = > {
    const ctx = useContext(RouterContext);
    returnctx.location? .state }// We need to get the state data of the params parameter using the context, but normally we get the nearest context.provider root node
// So go up one more layer in route<RouterContext.Provider value={{ ... ctx,match: result,
    }}
>
    {MatchCom}
</RouterContext.Provider>
    
// It is necessary. So what's passed in here is the result of the match and so forth that the child component can get

Copy the code

Code address: Github put here again

Just a simple implementation…

Just a simple implementation…

Just a simple implementation…

over