Possible to do page transitions?

So I just realized, while Frontity is built with React, it’s pretty different than a website made with react like ustwo.com because Frontity is server side rendered. Static websites etc make for really fast load times, but does that mean it’s not possible to do any preloading in the background or have it transition between pages?

Sure, you can do page transitions with Frontity.

@David and I did a little experiment with react-spring. It’s probably not perfect, but it’s a start :slight_smile:

What we did:

  1. We used the useTransition hook of react-spring and used state.router.link to trigger the new animations:
const transitions = useTransition(state.router.link, link => link, {
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 }
  });
  1. We put everything in <Body> inside a the transitions map returned by react-spring:
{transitions.map(({ item, props, key }) => {
        const data = state.source.get(item);
        return (
          <animated.div key={key} style={props}>
            <Absolute>
              <Body>
                {data.isFetching && <Loading />}
                {data.isArchive && <List data={data} />}
                {data.isPostType && <Post data={data} />}
                {data.is404 && <Page404 />}
                {data.content}
              </Body>
            </Absolute>
          </animated.div>
        );
      })}

Instead of getting our data from the state.router.link variable, we use the link (called item) that transitions gives us. react-spring takes care of storing the previous value for us.

  1. We passed that data to both <List> and <Post> components. They are not “hardcoded” to state.router.link anymore, they can render what you pass them in the data prop. This is needed because during the animation, both the new and the previous <Body> need to coexist, but render different links.

  2. We added a new styled component called <Absolute> that makes everything under <Body> absolute. This is needed because during the animation, both the new and the previous <Body> need to coexist and be on top of each other.

const Absolute = styled.div`
  position: absolute;
  width: 100%;
`;

As I said, it’s probably not perfect, but it’s a start :slight_smile:

3 Likes

Awesome, I’m going to take a crack at this. I don’t know react super well yet, but I think I can follow this gist of what you’ve got here, considering I converted a wordpress theme to use transitions using a vanilla javascript plugin previously and I have some sense of what’s going on here.

This seems to me something that might be good to introduce into the core functionality of frontity. For me the main reason for wanting a React frontend is because it can feel snappier due to the virtual dom and the ability to preload assets in the background. Page transitions are a big part of enhancing that snappiness and making it feel clean and not jarring.

1 Like

Absolutely :slight_smile:

It took us a while to understand how react-spring works but, once we got it, it was super simple.

Hey started trying to implement the transitions according to your codesandbox example today and I’m getting a fade in on stuff but I’m getting errors when I click a link.

This is probably because I’ve changed a lot of things in the them I’m working on (such as changing the name of “list.js” to “archive”, and some refactoring of the component structure in index.js according to what seemed simpler (such as turning HeadContainer>Header into just Header).

Here’s the errors I’m getting from the dev tools console:

Uncaught TypeError: Cannot read property 'map' of undefined
    at Archive (archive.js?e9fa:10)
    at runAsReaction (reactionRunner.js?30ae:54)
    at reaction (observer.js?724e:8)
    at eval (connect.js?84b5:70)
    at renderWithHooks (react-dom.development.js?61bb:16235)
    at updateFunctionComponent (react-dom.development.js?61bb:18310)
    at updateSimpleMemoComponent (react-dom.development.js?61bb:18257)
    at beginWork$1 (react-dom.development.js?61bb:20217)
    at HTMLUnknownElement.callCallback (react-dom.development.js?61bb:332)
    at eval (scheduler.js?f1c4:8)
Archive @ archive.js?e9fa:10
runAsReaction @ reactionRunner.js?30ae:54
reaction @ observer.js?724e:8
eval @ connect.js?84b5:70
renderWithHooks @ react-dom.development.js?61bb:16235
updateFunctionComponent @ react-dom.development.js?61bb:18310
updateSimpleMemoComponent @ react-dom.development.js?61bb:18257
beginWork$1 @ react-dom.development.js?61bb:20217
callCallback @ react-dom.development.js?61bb:332
eval @ scheduler.js?f1c4:8
batchedUpdates$1 @ react-dom.development.js?61bb:24344
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
invokeGuardedCallbackDev @ react-dom.development.js?61bb:384
invokeGuardedCallback @ react-dom.development.js?61bb:437
beginWork$$1 @ react-dom.development.js?61bb:25738
performUnitOfWork @ react-dom.development.js?61bb:24655
workLoopSync @ react-dom.development.js?61bb:24637
performSyncWorkOnRoot @ react-dom.development.js?61bb:24228
eval @ react-dom.development.js?61bb:12169
unstable_runWithPriority @ scheduler.development.js?3069:788
runWithPriority$2 @ react-dom.development.js?61bb:12071
flushSyncCallbackQueueImpl @ react-dom.development.js?61bb:12169
flushSyncCallbackQueue @ react-dom.development.js?61bb:12146
batchedUpdates$1 @ react-dom.development.js?61bb:24359
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
backend.js:6 The above error occurred in one of your React components:
    in Unknown (created by InnerLoadable)
    in InnerLoadable (created by Context.Consumer)
    in Unknown (created by ForwardRef)
    in ForwardRef
    in div (created by Context.Consumer)
    in Styled(div)
    in div (created by Context.Consumer)
    in Styled(div)
    in div (created by ForwardRef)
    in ForwardRef
    in Unknown (created by App)
    in HelmetProvider (created by App)
    in App

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.
r @ backend.js:6
logCapturedError @ react-dom.development.js?61bb:21810
logError @ react-dom.development.js?61bb:21810
update.callback @ react-dom.development.js?61bb:23192
callCallback @ react-dom.development.js?61bb:13794
commitUpdateEffects @ react-dom.development.js?61bb:13836
commitUpdateQueue @ react-dom.development.js?61bb:13836
commitLifeCycles @ react-dom.development.js?61bb:22102
commitLayoutEffects @ react-dom.development.js?61bb:25295
callCallback @ react-dom.development.js?61bb:332
eval @ scheduler.js?f1c4:8
batchedUpdates$1 @ react-dom.development.js?61bb:24344
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
invokeGuardedCallbackDev @ react-dom.development.js?61bb:384
invokeGuardedCallback @ react-dom.development.js?61bb:437
commitRootImpl @ react-dom.development.js?61bb:25035
unstable_runWithPriority @ scheduler.development.js?3069:788
runWithPriority$2 @ react-dom.development.js?61bb:12071
commitRoot @ react-dom.development.js?61bb:24840
finishSyncRender @ react-dom.development.js?61bb:24288
performSyncWorkOnRoot @ react-dom.development.js?61bb:24271
eval @ react-dom.development.js?61bb:12169
unstable_runWithPriority @ scheduler.development.js?3069:788
runWithPriority$2 @ react-dom.development.js?61bb:12071
flushSyncCallbackQueueImpl @ react-dom.development.js?61bb:12169
flushSyncCallbackQueue @ react-dom.development.js?61bb:12146
batchedUpdates$1 @ react-dom.development.js?61bb:24359
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
react-dom.development.js?61bb:12192 Uncaught TypeError: Cannot read property 'map' of undefined
    at Archive (archive.js?e9fa:10)
    at runAsReaction (reactionRunner.js?30ae:54)
    at reaction (observer.js?724e:8)
    at eval (connect.js?84b5:70)
    at renderWithHooks (react-dom.development.js?61bb:16235)
    at updateFunctionComponent (react-dom.development.js?61bb:18310)
    at updateSimpleMemoComponent (react-dom.development.js?61bb:18257)
    at beginWork$1 (react-dom.development.js?61bb:20217)
    at HTMLUnknownElement.callCallback (react-dom.development.js?61bb:332)
    at eval (scheduler.js?f1c4:8)
Archive @ archive.js?e9fa:10
runAsReaction @ reactionRunner.js?30ae:54
reaction @ observer.js?724e:8
eval @ connect.js?84b5:70
renderWithHooks @ react-dom.development.js?61bb:16235
updateFunctionComponent @ react-dom.development.js?61bb:18310
updateSimpleMemoComponent @ react-dom.development.js?61bb:18257
beginWork$1 @ react-dom.development.js?61bb:20217
callCallback @ react-dom.development.js?61bb:332
eval @ scheduler.js?f1c4:8
batchedUpdates$1 @ react-dom.development.js?61bb:24344
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
invokeGuardedCallbackDev @ react-dom.development.js?61bb:384
invokeGuardedCallback @ react-dom.development.js?61bb:437
beginWork$$1 @ react-dom.development.js?61bb:25738
performUnitOfWork @ react-dom.development.js?61bb:24655
workLoopSync @ react-dom.development.js?61bb:24637
performSyncWorkOnRoot @ react-dom.development.js?61bb:24228
eval @ react-dom.development.js?61bb:12169
unstable_runWithPriority @ scheduler.development.js?3069:788
runWithPriority$2 @ react-dom.development.js?61bb:12071
flushSyncCallbackQueueImpl @ react-dom.development.js?61bb:12169
flushSyncCallbackQueue @ react-dom.development.js?61bb:12146
batchedUpdates$1 @ react-dom.development.js?61bb:24359
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
backend.js:6 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
    in Unknown
    in div (created by Context.Consumer)
    in Styled(div)
    in div (created by Context.Consumer)
    in Styled(div)
    in div (created by ForwardRef)
    in ForwardRef
    in Unknown (created by App)
    in HelmetProvider (created by App)
    in App
r @ backend.js:6
warningWithoutStack @ react-dom.development.js?61bb:530
warnAboutUpdateOnUnmountedFiberInDEV @ react-dom.development.js?61bb:25684
scheduleUpdateOnFiber @ react-dom.development.js?61bb:23635
dispatchAction @ react-dom.development.js?61bb:17051
scheduler @ connect.js?84b5:52
queueReaction @ reactionRunner.js?30ae:25
queueReactionsForOperation @ reactionRunner.js?30ae:75
set @ handlers.js?c9ee:84
eval @ actions.ts?f1c8:48
asyncGeneratorStep @ actions.ts?f1c8:2
_next @ actions.ts?f1c8:2
eval @ scheduler.js?f1c4:8
batchedUpdates$1 @ react-dom.development.js?61bb:24344
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
Promise.then (async)
apply @ scheduler.js?f1c4:33
asyncGeneratorStep @ actions.ts?f1c8:2
_next @ actions.ts?f1c8:2
eval @ actions.ts?f1c8:2
eval @ actions.ts?f1c8:2
eval @ actions.ts?f1c8:58
eval @ create-store.js?81b0:1
eval @ actions.ts?fe69:12
eval @ create-store.js?81b0:1
onClick @ link.js?564e:11
callCallback @ react-dom.development.js?61bb:332
eval @ scheduler.js?f1c4:8
batchedUpdates$1 @ react-dom.development.js?61bb:24344
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
invokeGuardedCallbackDev @ react-dom.development.js?61bb:384
invokeGuardedCallback @ react-dom.development.js?61bb:437
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js?61bb:453
executeDispatch @ react-dom.development.js?61bb:581
executeDispatchesInOrder @ react-dom.development.js?61bb:606
executeDispatchesAndRelease @ react-dom.development.js?61bb:711
executeDispatchesAndReleaseTopLevel @ react-dom.development.js?61bb:711
forEachAccumulated @ react-dom.development.js?61bb:690
runEventsInBatch @ react-dom.development.js?61bb:732
runExtractedPluginEventsInBatch @ react-dom.development.js?61bb:864
handleTopLevel @ react-dom.development.js?61bb:5778
batchedEventUpdates$1 @ react-dom.development.js?61bb:24359
batchedEventUpdates @ react-dom.development.js?61bb:1413
dispatchEventForPluginEventSystem @ react-dom.development.js?61bb:5898
attemptToDispatchEvent @ react-dom.development.js?61bb:6011
dispatchEvent @ react-dom.development.js?61bb:5914
unstable_runWithPriority @ scheduler.development.js?3069:788
runWithPriority$2 @ react-dom.development.js?61bb:12071
discreteUpdates$1 @ react-dom.development.js?61bb:24384
discreteUpdates @ react-dom.development.js?61bb:1426
dispatchDiscreteEvent @ react-dom.development.js?61bb:5846
eval @ scheduler.js?f1c4:8
batchedUpdates$1 @ react-dom.development.js?61bb:24344
batch @ scheduler.js?f1c4:8
batched @ scheduler.js?f1c4:17
Show 26 more frames

I don’t want to seem like I’m dumping a bunch of trash on ya’ll btw. I’m not super experienced with react, and my javascript skills are still pretty rudimentary, but I’m doing my best to figure it out on my own as much as I can, doing a bunch of googling and continuing to try isolate the cause of the problem. If there’s more info ya’ll need let me know.

EDIT 1:

If I click one of the posts and it bugs out, I then can refresh the page and the page will load correct. But I do get the following error in the console:

react-dom.development.js?61bb:530 Warning: React does not recognize the `allowTransparency` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `allowtransparency` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
    in iframe
    in div
    in figure
    in Unknown
    in article (created by Context.Consumer)
    in Styled(article)
    in Unknown
    in div (created by Context.Consumer)
    in Styled(div)
    in div (created by Context.Consumer)
    in Styled(div)
    in div (created by ForwardRef)
    in ForwardRef
    in Unknown (created by App)
    in HelmetProvider (created by App)
    in App

EDIT2:

Okay so I’m diffing the current version of mars-theme I have downloaded against the codesandbox example you gave so that I can double check to see if you made any changes to files other than just index.js.

I found that one change was that post.js has const Post = ({ state, actions, libraries, data }) => { Since data is one of the things that we’re dealing with in the index I added that to my post.js. Now the app does something different! It still errors out when I try to go from the frontpage to a post, but when I’m at a frontpage and I click the home button it does correctly load it! IDK why… I feel kind of silly, like I’m cargo cult coding where I’m just copying and pasting things according to forms created by other people without any understanding but it’s the best I’ve got right now. I hope that by doing this I can at lest internalize a little more of react. I’m still such a novice at it.

EDIT 3:

WOOO!!! I got it working! It now transitions properly between posts and the archive page.

It seems the important thing is just that I delete the const data = state.source.get(state.router.link); and add data to ({ state, data }) .

Why? IDK…

But let me reason this outloud and maybe you can confirm.

Well before the component Post was getting it’s const data defined by the state.source.get(state.router.link).

Which broken down means:

state.source
source refers to wp-source right? So the component state contains the Wordpress data in the source object right?

.get
so this is a method of source to get:

state.router.link
I haven’t actually learned what routing is yet in react. I hear about it all the time but I’m not really clear on how it works or what it means.

But I can summazie that state is knowing what information to get via the state’s routing link object. yeah?

That’s how stuff USED to work.

But with the page transition stuff the way it works is that data is passed along with state. Where does data get defined?

It gets defined in state.source.get(item).

Where does state.source.get(item) get defined? it gets defined as a parameter of transitions.map? … I guess? I’m not quite sure how transitions.map knows where to get the argument to fill the item parameter. This is an aspect of a lot of javascript code that I’m still kind of iffy on understand overall tbh. Like I understand passing arguments in paramters of functions etc, in the abstract but how this works at a library/depency level in between stuff is still unclear to me.

I’m glad you finally got it working! :blush:

As you say, how Router and Source work is not clear right now. It’s one aspect we want to work on and we hope to release new documentation about this soon.

In the meantime, I’ll try to explain better your questions. When you create a project in Frontity, the routing is handled by the Frontity package @frontity/tiny-router, which basically let you set the url with actions.router.set() and detects where are you and stores it at state.router.link.

State is an object where we store all the data necessary, and in state.source we are storing all the data related to the API where you are getting data from, in this case wp-source (although you could create a new one if you want).

Screenshot%20from%202019-11-13%2013-06-40

You can take a deeper look at it going to your website (or mars.frontity.org), opening the dev tools and writing in the console frontity.state.source.

There you could find all the data about the posts, categories, authors, etc… you fetch from WordPress. And also another important object named data.

In order to make it easier and preventing you from learning WP REST API, we decided to manage this data using URLs instead of the common endpoints. This means that in source.data we store the important data in the urls: if it has been fetched, if it’s home, if it’s a post, a category, the id, etc.
Screenshot%20from%202019-11-13%2013-13-59
From there, you can look for the rest of the info at the state when needed.

If you want to access the data of one url, you can use the method get with the url as an argument. For example, if we want to get the data from "/category/nature/" we just have to use: state.source.get("/category/nature/") and we’ll be accessing state.source.data["/category/nature/"].

Note that in order to have the data available in the state, is has to be fetched before with actions.source.fetch() . By default Frontity does the fetch once you access one url, but if you change that behavior you have to make sure to do the fetch before getting the data.

So let’s try to explain your example too: const data = state.source.get(state.router.link)
We are basically getting the data of the current url. state.route.link, as explained before, gives you the current url, and state.source.get() gets the data from the state.

I hope this info is enough to give you a better understanding about how does it work and help you with the page transitions. We’ll inform you when we have better documentation for this.

Please let us know if you have more questions :slightly_smiling_face: