React Router переходы в и из событий - PullRequest
0 голосов
/ 23 ноября 2018

У меня довольно простая настройка для небольшого веб-сайта, над которым я работаю.Я использую React и React Router 4. Теперь я хотел бы добавить переход к тому, когда пользователь вводит маршрут, к переходу IN и OUT этого маршрута с некоторой анимацией JavaScript.Тем не менее, я не могу понять, как это сделать правильно?Допустим, пользователь находится в / и щелкает ссылку, которая ведет к / projects / one, тогда как я могу затем начать переход IN для этого, и если пользователь уходит, чтобы начать переход OUT для этого компонента / маршрута?Я не хочу, чтобы вещи просто «размонтировались», я хочу, чтобы они были плавными между переходами и имели контроль ..?Значение времени ожидания приведено только в качестве примера.

На данный момент у меня есть следующее:

ОБНОВЛЕНИЕ:

На основе примера кода Ryan C IЯ смог найти решение, которое очень близко подошло к тому, что я хотел бы получить, и тем самым удалило мой старый код, поскольку он был слишком далек от моего первоначального вопроса.

Код: https://codesandbox.io/s/k2r02r378o

Для этой текущей версии у меня есть два вопроса, которые я не могу выяснить ...

  1. Если пользовательв настоящее время на HomePage (/) и пользователь нажимает на ссылку для того же пути, как я могу предотвратить мой переходный процесс, и просто ничего не делать?И в то же время не добавлять много истории с одним и тем же путем в браузере?

  2. Если пользователь находится на HomePage (/) и переходит на ProjectsPage (/ projects / one),и до того, как переход завершится, пользователь снова вернется к HomePage (/), затем я бы хотел, чтобы «transitionOut» HomePage остановился там, где он есть, и снова запустил «transitionIn» (что-то вроде перемотки моего перехода из твин).это связано с 1)?

Ответы [ 2 ]

0 голосов
/ 23 декабря 2018

Таким образом, оказывается, что подход, который поддерживает перезапуск входящего перехода, если вы переключаетесь с маршрута 1 на маршрут 2, а затем обратно на маршрут 1, когда маршрут 1 все еще выходит, довольно сложен.Могут быть некоторые незначительные проблемы в том, что у меня есть, но я думаю, что общий подход оправдан.

Общий подход включает в себя отделение целевого пути (куда пользователь хочет идти) от пути рендеринга (этот путьв настоящее время отображается, который может быть в переходном состоянии).Чтобы убедиться, что переход происходит в подходящее время, используется состояние для последовательной последовательности действий (например, сначала визуализируйте переход с помощью in=false, а затем визуализируйте с in=true для входящего перехода).Большая часть сложности обрабатывается в TransitionManager.js.

. Я использовал хуки в своем коде, потому что мне было легче работать с логикой без дополнительных затрат на синтаксис классов, так что в течение следующих нескольких месяцевили так, это будет работать только с альфа.Если реализация хуков изменится в официальном выпуске каким-либо образом, нарушающим этот код, я обновлю этот ответ в то время.

Вот код:

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

App.js

import React from "react";
import { BrowserRouter } from "react-router-dom";
import LinkOrStatic from "./LinkOrStatic";
import { componentInfoArray } from "./components";
import {
  useTransitionContextState,
  TransitionContext
} from "./TransitionContext";
import TransitionRoute from "./TransitionRoute";

const App = props => {
  const transitionContext = useTransitionContextState();
  return (
    <TransitionContext.Provider value={transitionContext}>
      <BrowserRouter>
        <div>
          <br />
          {componentInfoArray.map(compInfo => (
            <LinkOrStatic key={compInfo.path} to={compInfo.path}>
              {compInfo.linkText}
            </LinkOrStatic>
          ))}

          {componentInfoArray.map(compInfo => (
            <TransitionRoute
              key={compInfo.path}
              path={compInfo.path}
              exact
              component={compInfo.component}
            />
          ))}
        </div>
      </BrowserRouter>
    </TransitionContext.Provider>
  );
};
export default App;

TransitionContext.js

import React, { useState } from "react";

export const TransitionContext = React.createContext();
export const useTransitionContextState = () => {
  // The path most recently requested by the user
  const [targetPath, setTargetPath] = useState(null);
  // The path currently rendered. If different than the target path,
  // then probably in the middle of a transition.
  const [renderInfo, setRenderInfo] = useState(null);
  const [exitTimelineAndDone, setExitTimelineAndDone] = useState({});
  const transitionContext = {
    targetPath,
    setTargetPath,
    renderInfo,
    setRenderInfo,
    exitTimelineAndDone,
    setExitTimelineAndDone
  };
  return transitionContext;
};

components.js

import React from "react";
const Home = props => {
  return <div>Hello {props.state + " Home!"}</div>;
};
const ProjectOne = props => {
  return <div>Hello {props.state + " Project One!"}</div>;
};
const ProjectTwo = props => {
  return <div>Hello {props.state + " Project Two!"}</div>;
};
export const componentInfoArray = [
  {
    linkText: "Home",
    component: Home,
    path: "/"
  },
  {
    linkText: "Show project one",
    component: ProjectOne,
    path: "/projects/one"
  },
  {
    linkText: "Show project two",
    component: ProjectTwo,
    path: "/projects/two"
  }
];

LinkOrStatic.js

import React from "react";
import { Route, Link } from "react-router-dom";

const LinkOrStatic = props => {
  const path = props.to;
  return (
    <>
      <Route exact path={path}>
        {({ match }) => {
          if (match) {
            return props.children;
          }
          return (
            <Link className={props.className} to={props.to}>
              {props.children}
            </Link>
          );
        }}
      </Route>
      <br />
    </>
  );
};
export default LinkOrStatic;

TransitionRoute.js

import React from "react";
import { Route } from "react-router-dom";
import TransitionManager from "./TransitionManager";

const TransitionRoute = props => {
  return (
    <Route path={props.path} exact>
      {({ match }) => {
        return (
          <TransitionManager
            key={props.path}
            path={props.path}
            component={props.component}
            match={match}
          />
        );
      }}
    </Route>
  );
};
export default TransitionRoute;

TransitionManager.js

import React, { useContext, useEffect } from "react";
import { Transition } from "react-transition-group";
import {
  slowFadeInAndDropFromAboveThenLeftRight,
  slowFadeOutAndDrop
} from "./animations";
import { TransitionContext } from "./TransitionContext";

const NEW_TARGET = "NEW_TARGET";
const NEW_TARGET_MATCHES_EXITING_PATH = "NEW_TARGET_MATCHES_EXITING_PATH";
const FIRST_TARGET_NOT_RENDERED = "FIRST_TARGET_NOT_RENDERED";
const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED =
  "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED";
const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING =
  "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING";
const TARGET_RENDERED = "TARGET_RENDERED";
const NOT_TARGET_AND_NEED_TO_START_EXITING =
  "NOT_TARGET_AND_NEED_TO_START_EXITING";
const NOT_TARGET_AND_EXITING = "NOT_TARGET_AND_EXITING";
const NOT_TARGET = "NOT_TARGET";
const usePathTransitionCase = (path, match) => {
  const {
    targetPath,
    setTargetPath,
    renderInfo,
    setRenderInfo,
    exitTimelineAndDone,
    setExitTimelineAndDone
  } = useContext(TransitionContext);
  let pathTransitionCase = null;
  if (match) {
    if (targetPath !== path) {
      if (
        renderInfo &&
        renderInfo.path === path &&
        renderInfo.transitionState === "exiting" &&
        exitTimelineAndDone.timeline
      ) {
        pathTransitionCase = NEW_TARGET_MATCHES_EXITING_PATH;
      } else {
        pathTransitionCase = NEW_TARGET;
      }
    } else if (renderInfo === null) {
      pathTransitionCase = FIRST_TARGET_NOT_RENDERED;
    } else if (renderInfo.path !== path) {
      if (renderInfo.transitionState === "exited") {
        pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED;
      } else {
        pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING;
      }
    } else {
      pathTransitionCase = TARGET_RENDERED;
    }
  } else {
    if (renderInfo !== null && renderInfo.path === path) {
      if (
        renderInfo.transitionState !== "exiting" &&
        renderInfo.transitionState !== "exited"
      ) {
        pathTransitionCase = NOT_TARGET_AND_NEED_TO_START_EXITING;
      } else {
        pathTransitionCase = NOT_TARGET_AND_EXITING;
      }
    } else {
      pathTransitionCase = NOT_TARGET;
    }
  }
  useEffect(() => {
    switch (pathTransitionCase) {
      case NEW_TARGET_MATCHES_EXITING_PATH:
        exitTimelineAndDone.timeline.kill();
        exitTimelineAndDone.done();
        setExitTimelineAndDone({});
        // Making it look like we exited some other path, in
        // order to restart the transition into this path.
        setRenderInfo({
          path: path + "-exited",
          transitionState: "exited"
        });
        setTargetPath(path);
        break;
      case NEW_TARGET:
        setTargetPath(path);
        break;
      case FIRST_TARGET_NOT_RENDERED:
        setRenderInfo({ path: path });
        break;
      case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
        setRenderInfo({ path: path, transitionState: "entering" });
        break;
      case NOT_TARGET_AND_NEED_TO_START_EXITING:
        setRenderInfo({ ...renderInfo, transitionState: "exiting" });
        break;
      // case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
      // case NOT_TARGET:
      default:
      // no-op
    }
  });
  return {
    renderInfo,
    setRenderInfo,
    setExitTimelineAndDone,
    pathTransitionCase
  };
};

const TransitionManager = props => {
  const {
    renderInfo,
    setRenderInfo,
    setExitTimelineAndDone,
    pathTransitionCase
  } = usePathTransitionCase(props.path, props.match);
  const getEnterTransition = show => (
    <Transition
      key={props.path}
      addEndListener={slowFadeInAndDropFromAboveThenLeftRight()}
      in={show}
      unmountOnExit={true}
    >
      {state => {
        const Child = props.component;
        console.log(props.path + ": " + state);
        return <Child state={state} />;
      }}
    </Transition>
  );
  const getExitTransition = () => {
    return (
      <Transition
        key={props.path}
        addEndListener={slowFadeOutAndDrop(setExitTimelineAndDone)}
        in={false}
        onExited={() =>
          setRenderInfo({ ...renderInfo, transitionState: "exited" })
        }
        unmountOnExit={true}
      >
        {state => {
          const Child = props.component;
          console.log(props.path + ": " + state);
          return <Child state={state} />;
        }}
      </Transition>
    );
  };
  switch (pathTransitionCase) {
    case NEW_TARGET_MATCHES_EXITING_PATH:
    case NEW_TARGET:
    case FIRST_TARGET_NOT_RENDERED:
    case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
      return null;
    case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
      return getEnterTransition(false);
    case TARGET_RENDERED:
      return getEnterTransition(true);
    case NOT_TARGET_AND_NEED_TO_START_EXITING:
    case NOT_TARGET_AND_EXITING:
      return getExitTransition();
    // case NOT_TARGET:
    default:
      return null;
  }
};
export default TransitionManager;

animations.js

import { TimelineMax } from "gsap";
const startStyle = { autoAlpha: 0, y: -50 };
export const slowFadeInAndDropFromAboveThenLeftRight = trackTimelineAndDone => (
  node,
  done
) => {
  const timeline = new TimelineMax();
  if (trackTimelineAndDone) {
    trackTimelineAndDone({ timeline, done });
  }
  timeline.set(node, startStyle);
  timeline
    .to(node, 0.5, {
      autoAlpha: 1,
      y: 0
    })
    .to(node, 0.5, { x: -25 })
    .to(node, 0.5, {
      x: 0,
      onComplete: done
    });
};
export const slowFadeOutAndDrop = trackTimelineAndDone => (node, done) => {
  const timeline = new TimelineMax();
  if (trackTimelineAndDone) {
    trackTimelineAndDone({ timeline, done });
  }
  timeline.to(node, 2, {
    autoAlpha: 0,
    y: 100,
    onComplete: done
  });
};

Edit nrnz2lvnxj

0 голосов
/ 24 ноября 2018

Я оставил этот ответ на месте, чтобы комментарии все еще имели смысл, и эволюция была видна, но это было заменено моим новым ответом

Вот несколько ссылок, на которые, я полагаю, вы смотрели:

https://reacttraining.com/react-router/web/api/Route/children-func

https://reactcommunity.org/react-transition-group/transition

https://greensock.com/react

code sandbox с кодом, приведенным ниже, чтобы вы могли быстро увидеть эффект

В приведенном ниже коде используется Transition в a Route с использованием свойства addEndListener для подключенияпользовательская анимация с использованием gsap.Есть несколько важных аспектов для этой работы.Чтобы Transition проходил через состояние entering, свойство in должно иметь значение от false до true.Если он начинается с true, он сразу же перейдет в состояние entered без перехода.Для того чтобы это произошло в Route, вам необходимо использовать свойство children Маршрута (а не component или render), так как тогда дочерние элементы будут отображаться независимо от того, совпадает ли Маршрут.В приведенном ниже примере вы увидите:

<Route exact path="/projects/one">
    {({ match }) => <Projects show={match !== null} />}
</Route>

Это передает логическое свойство show компоненту, которое будет иметь значение true, только если маршрут соответствует.Затем он будет передан как in свойство Transition.Это позволяет Projects начинаться с in={false}, а не (при использовании свойства Route component) начинаться как вообще не отображаемым (что предотвратит переход, потому что тогда он будет иметь in={true}, когда он первыйрендеринг).

Я не полностью переварил все, что вы пытались сделать в componentDidMount проектов (мой пример значительно упрощен, но выполняет многошаговую анимацию gsap), но я думаю,вам будет лучше использовать Transition для управления запуском всех ваших анимаций, чем пытаться использовать оба Transition и componentDidMount.

Вот 1-я версия кода:

import React from "react";
import ReactDOM from "react-dom";
import { Transition } from "react-transition-group";
import { BrowserRouter, Route, Link } from "react-router-dom";
import { TweenLite, TimelineMax } from "gsap";

const startState = { autoAlpha: 0, y: -50 };
const onEnter = node => TweenLite.set(node, startState);
const addEndListener = props => (node, done) => {
  const timeline = new TimelineMax();
  if (props.show) {
    timeline
      .to(node, 0.5, {
        autoAlpha: 1,
        y: 0
      })
      .to(node, 0.5, { x: -25 })
      .to(node, 0.5, {
        x: 0,
        onComplete: done
      });
  } else {
    timeline.to(node, 0.5, {
      autoAlpha: 0,
      y: 50,
      onComplete: done
    });
  }
};
const Home = props => {
  return (
    <Transition
      unmountOnExit
      in={props.show}
      onEnter={onEnter}
      addEndListener={addEndListener(props)}
    >
      {state => {
        return <div>Hello {state + " Home!"}</div>;
      }}
    </Transition>
  );
};
const Projects = props => {
  return (
    <Transition
      unmountOnExit
      in={props.show}
      onEnter={onEnter}
      addEndListener={addEndListener(props)}
    >
      {state => {
        return <div>Hello {state + " Projects!"}</div>;
      }}
    </Transition>
  );
};

const App = props => {
  return (
    <BrowserRouter>
      <div>
        <br />
        <Link to="/">Home</Link>
        <br />
        <Link to="/projects/one">Show project</Link>
        <br />
        <Route exact path="/">
          {({ match }) => <Home show={match !== null} />}
        </Route>
        <Route exact path="/projects/one">
          {({ match }) => <Projects show={match !== null} />}
        </Route>
      </div>
    </BrowserRouter>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Обновление 1: Решение вопроса № 1 в вашем обновлении.Одна из приятных вещей в реакции-роутере версии 4 заключается в том, что маршруты могут появляться в нескольких местах и ​​управлять несколькими частями страницы.В этой кодовой изолированной программной среде я обновил вашу изолированную программную среду кода, чтобы ссылка на домашнюю страницу переключалась между ссылкой и статическим текстом (хотя вы могли бы изменить это, чтобы использовать стили, чтобы внешний вид был одинаковым дляи то и другое).Я заменил Link на LinkOrStaticText (я сделал это быстро, и он мог бы использовать некоторые уточнения для более надежной обработки прохождения пропусков):

const LinkOrStatic = props => {
  const path = props.to;
  return (
    <Route exact path={path}>
      {({ match }) => {
        if (match) {
          return props.children;
        }
        return (
          <Link className={props.className} to={props.to}>
            {props.children}
          </Link>
        );
      }}
    </Route>
  );
};

Я сделаю отдельное обновление для решения вопроса 2.

Обновление 2 : пытаясь ответить на вопрос 2, я обнаружил некоторые фундаментальные проблемы с подходом, который я использовал в этом ответе.Поведение становилось запутанным из-за одновременного выполнения нескольких маршрутов в определенных случаях и из-за нечетных остатков незавершенных переходов, которые выполнялись.Мне нужно было начать сначала с нуля с другим подходом, поэтому я публикую пересмотренный подход в отдельном ответе.

...