TylerBarnes / gatsby-plugin-transition-link

A link component for page transitions in gatsby
537 stars 70 forks source link

Pass additional props from Layout to page component #211

Closed TheoGil closed 4 years ago

TheoGil commented 4 years ago

This is more of a question/how to than a actual bug.

I'm using this plugin with the layout option and would like to send data from the state of the Layout component to the pages components.

Ultimately, what I want, is to perfom an "introduction animation" from Layout component, that will only run once upon the initial page load. This animation will move around the persistent elements that are located in Layout. Once this animation is complete, I want to animate in the page content.

Something like:

  1. Initial page load
  2. Layout plays intro animation
  3. Intro animation complete, update Layout state
  4. Page component gets notified of the Layout update, animate in content

I can't figure out the proper way to do this though. I've managed to pass extra props to the page component but his break the page transitions, as my page component isn't wrapped in the Transitions components anymore...

// ./src/components/layout.js
export default class Layout extends React.Component {
constructor(props) {
    super(props)
    this.state = {
      intro: true,
    }
  }

  render() {
    const PageComponent = this.props.pageResources.component

    return (
      <div>
        <Header />

        <PageComponent intro={this.state.intro} />

        <ContactBadge />
      </div>
    )
  }
}

I've also taken a look at React Context, as advised here (Passing data from Layout to Page is exactly what i want to achieve). But I do not see how I can apply this in my case, as I want to be able to read the data from the componentDidMount/componentDidUpdate methods of the page component

edit: I've tried to use this cleaner solution from this stack overflow post, this does not break the page transition, but unfortunately the additional intro prop is only passed to the TransitionHandler component and not its children (ie my page component).

// ./src/components/layout.js
import React from "react"
import Header from "./header"
import ContactBadge from "./contact_badge/contact"

export default class Layout extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      intro: true,
    }
  }

  render() {
    return (
      <div>
        <Header />

        <main>
          {React.cloneElement(this.props.children, {
            intro: this.state.intro,
          })}
        </main>

        <ContactBadge />
      </div>
    )
  }
}
TheoGil commented 4 years ago

I have finally managed to pass extra data from the Layout to the page component using React Context. It turns out that everything that I needed was to dive into the documentation and some trial and error.

The following ressources gave me all the information that I needed to get this working:

I'm going to describe below the changes that I've made to my app to get this working, in case it could help someone in the future. To who it may concern, I'm not an expert in React or Gatsby or the use of Context, this might not be the most elegant way to do it but it works for my case. If you're in a rush or just like to copy paste code from strangers (😁), there you go!

  1. Create the context file
    
    // ./src/context/IntroContext.js
    import React from "react"

// Note: I'm not passing a defaultValue to createContext // as the whole app is going to be wrapped into the IntroProvider using the browser API wrapRootElement const IntroContext = React.createContext()

class IntroProvider extends React.Component { state = { shouldPlayIntro: true, }

update = newState => { this.setState(newState) }

render() { return ( <IntroContext.Provider value={{ shouldPlayIntro: this.state.shouldPlayIntro, update: this.update, }}

{this.props.children} </IntroContext.Provider> ) } } export default IntroContext export { IntroProvider }


2. Use the `wrapRootElement` method of the Gatsby Browser API to wrap our whole app into the `IntroProvider`. This will allow any child component to access the `IntroContext`
```js
// ./gatsby-browser.js
import React from "react"
import { IntroProvider } from "./src/context/IntroContext"

export function wrapRootElement({ element }) {
  return <IntroProvider>{element}</IntroProvider>
}
  1. Now that the IntroContext is available to our components, we can read and update the data it holds. In this example, I'm in the Layout component but you can do it in every component (pages and so on)
    
    // ./src/components/layout.js
    import React from "react"
    import IntroContext from "../context/IntroContext"
    import Header from "./header" // unrelated
    import ContactBadge from "./contact_badge/contact" // unrelated

export default class Layout extends React.Component { static contextType = IntroContext // lets you consume the nearest current value of that Context type using this.context

componentDidMount() { console.log(this.context.shouldPlayIntro) // true

setTimeout(() => {
  this.context.update({ shouldPlayIntro: false })
  console.log(this.context.shouldPlayIntro) // false
}, 2000)

}

render() { return (

{this.props.children}
)

} }


4. When in my page component, I listen to context changes by storing a reference to the previous state, and comparing the previous and new state in the `componentDidUpdate` method
```js
// ./src/pages/index.js
import React from "react"
import IntroContext from "../context/IntroContext"

export default class Index extends React.PureComponent {
  static contextType = IntroContext
  static previousContext

componentDidMount() {
    this.previousContext = this.context
  }

componentDidUpdate() {
    if (this.previousContext.shouldPlayIntro !== this.context.shouldPlayIntro) {
      this.previousContext = this.context
      // the context shouldPlayIntro property has changed, do some stuff
    }
  }
}