Coder Social home page Coder Social logo

UIRouter and React StrictMode about react HOT 6 CLOSED

wallzero avatar wallzero commented on April 28, 2024
UIRouter and React StrictMode

from react.

Comments (6)

christopherthielen avatar christopherthielen commented on April 28, 2024 2

@wallzero https://github.com/ui-router/react/releases/tag/0.8.10 sorry for the wait

from react.

wallzero avatar wallzero commented on April 28, 2024

Unless anyone objects, I'll work on a PR to migrate @uirouter/react to use hooks and test for compatibility with StrictMode.

from react.

wallzero avatar wallzero commented on April 28, 2024

So far I was able to update the UIRouter component and it works:

/**
 * @reactapi
 * @module components
 */ /** */
import * as React from 'react';
import { FunctionComponent, ReactElement, ValidationMap } from 'react';
import * as PropTypes from 'prop-types';

import { servicesPlugin } from '@uirouter/core';

import { UIRouterReact, ReactStateDeclaration } from '../index';

export const {
  /** @internalapi */
  Provider: UIRouterProvider,
  /**
   * <UIRouterConsumer> component lets you access the UIRouter instance
   * anywhere in the component tree, by simply wrapping your component and
   * using the function-as-child pattern to pass the instance via props.
   *
   * #### Example:
   * ```jsx
   * <UIRouterConsumer>
   *  {router => <MyComponent router={router} />}
   * </UIRouterConsumer>
   * ```
   */
  Consumer: UIRouterConsumer,
} = React.createContext<UIRouterReact>(undefined);

export interface UIRouterProps {
  children?: any;
  plugins?: any[]; // should fix type
  states?: ReactStateDeclaration[];
  config?: (router: UIRouterReact) => void;
  router?: UIRouterReact;
}

/** @hidden */
export const InstanceOrPluginsMissingError = new Error(`Router instance or plugins missing.
You must either provide a location plugin via the plugins prop:

<UIRouter plugins={[pushStateLocationPlugin]} states={[···]}>
  <UIView />
</UIRouter>

or initialize the router yourself and pass the instance via props:

const router = new UIRouterReact();
router.plugin(pushStateLocationPlugin);
···
<UIRouter router={router}>
  <UIView />
</UIRouter>
`);

/** @hidden */
export const UIRouterInstanceUndefinedError = new Error(
  `UIRouter instance is undefined. Did you forget to include the <UIRouter> as root component?`
);

const UIRouter: FunctionComponent<UIRouterProps> = ({
  plugins,
  states,
  config,
  children,
  router: propRouter,
}: UIRouterProps): ReactElement => {
  const router = React.useRef<UIRouterReact>();
  const [, forceUpdate] = React.useState();

  React.useEffect((): void => {
    // check if a router instance is provided
    if (router) {
      router.current = propRouter;
    } else if (plugins) {
      router.current = new UIRouterReact();
      router.current.plugin(servicesPlugin);
      plugins.forEach(plugin => router.current.plugin(plugin));
      if (config) config(router.current);
      (states || []).forEach(state => router.current.stateRegistry.register(state));
    } else {
      throw InstanceOrPluginsMissingError;
    }
    router.current.start();
    forceUpdate({});
  }, []);

  return router.current ? <UIRouterProvider value={router.current}>{children}</UIRouterProvider> : null;
};

UIRouter.propTypes = {
  plugins: PropTypes.arrayOf(PropTypes.func),
  states: PropTypes.arrayOf(PropTypes.object),
  config: PropTypes.func,
  children: PropTypes.element.isRequired,
  router: PropTypes.any,
} as ValidationMap<UIRouterProps>;

export { UIRouter };

I then tried updating UIView but am stuck. I don't receive any errors but it isn't rendering:

/**
 * @reactapi
 * @module components
 */ /** */
import * as React from 'react';
import {
  ClassType,
  ClassicComponentClass,
  Component,
  ComponentClass,
  FunctionComponent,
  ReactChildren,
  ReactElement,
  StatelessComponent,
  ValidationMap,
  Validator,
  cloneElement,
  createElement,
  isValidElement,
} from 'react';
import * as PropTypes from 'prop-types';

import { ActiveUIView, ViewContext, Transition, ResolveContext, StateParams, applyPairs } from '@uirouter/core';

import { UIRouterReact, UIRouterConsumer } from '../index';
import { ReactViewConfig } from '../reactViews';
import { UIRouterInstanceUndefinedError } from './UIRouter';

/** @internalapi */
let id = 0;

/** @internalapi */
export interface UIViewAddress {
  context: ViewContext;
  fqn: string;
}

/**
 * Interface for [[InjectedProps.resolves]]
 *
 * This Typescript interface shows what fields are available on the `resolves` field.
 */
export interface UIViewResolves {
  /**
   * Any key/value pair defined by a state's resolve
   *
   * If a state defines any [[ReactStateDeclaration.resolve]]s, they will be found on this object.
   */
  [key: string]: any;
  /**
   * The `StateParams` for the `Transition` that activated the component
   *
   * This is an alias for:
   * ```js
   * let $stateParams = $transition$.params("to");
   * ```
   */
  $stateParams: StateParams;
  /** The `Transition` that activated the component */
  $transition$: Transition;
}

/**
 * Function type for [[UIViewProps.render]]
 *
 * If the `render` function prop is provided, the `UIView` will use it instead of rendering the component by itself.
 * @internalapi
 */
export type RenderPropCallback = (
  Component: StatelessComponent<any> | ComponentClass<any> | ClassicComponentClass<any>,
  Props: any
) => JSX.Element | null;

export interface UIViewInjectedProps {
  transition?: Transition;
  resolves?: UIViewResolves;
  className?: string;
  style?: Object;
}

/** Component Props for `UIView` */
export interface UIViewProps {
  children?: ReactChildren;
  router?: UIRouterReact;
  parentUIView?: UIViewAddress;
  name?: string;
  className?: string;
  style?: Object;
  render?: RenderPropCallback;
}

/** Component State for `UIView` */
export interface UIViewState {
  id?: number;
  loaded?: boolean;
  component?: string | FunctionComponent<any> | ClassType<any, any, any> | ComponentClass<any>;
  props?: any;
}

export const TransitionPropCollisionError = new Error(
  '`transition` cannot be used as resolve token. ' +
    'Please rename your resolve to avoid conflicts with the router transition.'
);

/** @internalapi */
export const { Provider: UIViewProvider, Consumer: UIViewConsumer } = React.createContext<UIViewAddress>(undefined);

const View: FunctionComponent<UIViewProps> = ({
  children,
  router,
  parentUIView,
  name = '$default',
  className,
  style,
  render,
}: UIViewProps): ReactElement => {
  // This object contains all the metadata for this UIView
  const uiViewData = React.useRef<ActiveUIView>();

  // This object contains only the state context which created this <UIView/> component
  // and the UIView's fully qualified name. This object is made available to children via `context`
  const uiViewAddress = React.useRef<UIViewAddress>();

  // Deregisters the UIView when it is unmounted
  const deregister = React.useRef<Function>();

  // Bind the rendered component instance in order to call its uiCanExit hook
  const componentInstance = React.useRef<any>();
  // Removes th Hook when the UIView is unmounted
  const removeHook = React.useRef<Function>();

  const [state, setState] = React.useState<{
    id?: number;
    loaded?: boolean;
    component?: string | FunctionComponent<any> | ClassType<any, any, any> | ComponentClass<any>;
    props?: any;
  }>({
    loaded: false,
    component: 'div',
    props: {},
  });

  /**
   * View config updated callback
   *
   * This is called by UI-Router during ViewService.sync().
   * The `newConfig` parameter will contain view configuration (component, etc) when a
   * state is activated and one of its views targets this `UIView`.
   */
  const viewConfigUpdated = (newConfig: ReactViewConfig) => {
    if (newConfig === uiViewData.current.config) {
      return;
    }

    let trans: Transition;
    let resolves = {};

    if (newConfig) {
      let viewContext: ViewContext = newConfig.viewDecl && newConfig.viewDecl.$context;
      uiViewAddress.current = {
        fqn: uiViewAddress.current.fqn,
        context: viewContext,
      };

      let resolveContext = new ResolveContext(newConfig.path);
      let injector = resolveContext.injector();

      let stringTokens: string[] = resolveContext.getTokens().filter(x => typeof x === 'string');
      if (stringTokens.indexOf('transition') !== -1) {
        throw TransitionPropCollisionError;
      }

      trans = injector.get(Transition);
      resolves = stringTokens.map(token => [token, injector.get(token)]).reduce(applyPairs, {});
    }

    uiViewData.current.config = newConfig;
    const key = Date.now();
    let props = { ...resolves, transition: trans, key };

    let newComponent = newConfig && newConfig.viewDecl && newConfig.viewDecl.component;
    setState({
      ...state,
      component: newComponent || 'div',
      props: newComponent ? props : {},
      loaded: !!newComponent,
    });
  };

  const registerUiCanExitHook = (stateName: string) => {
    typeof removeHook.current === 'function' && removeHook.current();
    let criteria = { exiting: stateName };
    let callback =
      componentInstance.current &&
      typeof componentInstance.current.uiCanExit === 'function' &&
      componentInstance.current.uiCanExit;
    if (stateName && callback) {
      let transitions = router.transitionService;
      removeHook.current = transitions.onBefore(criteria, callback, {});
    }
  };

  React.useEffect((): (() => void) => {
    if (typeof router === 'undefined') {
      throw UIRouterInstanceUndefinedError;
    }

    // Check the context for the parent UIView's fqn and State
    let parent: UIViewAddress = parentUIView;
    // Not found in context, this is a root UIView
    parent = parent || { fqn: '', context: router.stateRegistry.root() };

    uiViewData.current = {
      $type: 'react',
      id: ++id,
      name: name,
      fqn: parent.fqn ? parent.fqn + '.' + name : name,
      creationContext: parent.context,
      configUpdated: viewConfigUpdated,
      config: undefined,
    } as ActiveUIView;

    uiViewAddress.current = { fqn: uiViewData.current.fqn, context: undefined };

    deregister.current = router.viewService.registerUIView(uiViewData.current);

    setState({
      ...state,
      id: uiViewData.current.id,
    });

    return (): void => {
      deregister.current();
    };
  }, []);

  let { component, props, loaded } = state;
  // register reference of child component
  // register new hook right after component has been rendered
  let stateName: string = uiViewAddress.current && uiViewAddress.current.context && uiViewAddress.current.context.name;

  // only class components can implement the
  // uiCanExit hook and ref doesn't work on
  // stateless function components
  if (typeof component !== 'string' && (!!component.render || (component.prototype && !!component.prototype.render))) {
    props.ref = c => {
      componentInstance.current = c;
      registerUiCanExitHook(stateName);
    };
  }

  // attach any style or className to the rendered component
  // specified on the UIView itself
  const styleProps = { className, style };
  const childProps = { ...props, ...styleProps };

  let child =
    !loaded && isValidElement(children) ? cloneElement(children, childProps) : createElement(component, childProps);

  // if a render function is passed use that,
  // otherwise render the component normally
  const ChildOrRenderFunction = typeof render !== 'undefined' && loaded ? render(component, childProps) : child;
  return <UIViewProvider value={uiViewAddress.current}>{ChildOrRenderFunction}</UIViewProvider>;
};

View.propTypes = {
  router: PropTypes.object.isRequired as Validator<UIRouterReact>,
  parentUIView: PropTypes.object as Validator<UIViewAddress>,
  name: PropTypes.string,
  className: PropTypes.string,
  style: PropTypes.object,
  render: PropTypes.func,
} as ValidationMap<UIViewProps>;

export class UIView extends React.Component<UIViewProps, any> {
  static displayName = 'UIView';
  static __internalViewComponent: React.FunctionComponent<UIViewProps> = View;

  render() {
    return (
      <UIRouterConsumer>
        {router => (
          <UIViewConsumer>
            {parentUIView => <View {...this.props} router={router} parentUIView={parentUIView} />}
          </UIViewConsumer>
        )}
      </UIRouterConsumer>
    );
  }
}

Here's my branch with recent changes.

from react.

wallzero avatar wallzero commented on April 28, 2024

Refactoring with hooks wasn't necessary to resolve the StrictMode issues.

from react.

christopherthielen avatar christopherthielen commented on April 28, 2024

Thanks for doing this work! I'll merge your PR when it's rebased against the latest dependency updates

from react.

wallzero avatar wallzero commented on April 28, 2024

Hey @christopherthielen I am just curious when the next release is scheduled? I have a couple merges myself which are waiting on #512 and #430. Thanks!

from react.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.