LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

Organization

Pallalax Hero

先日のサンプルは画面内判定で toggle class を発火するだけのものでした。本日は Hero image を header に変形させるサンプルの紹介です。スクロール 1px 単位で要素矩形を変形させます。
code: github / $ yarn 1208

1208.jpg 1208-1.jpg 1208-2.jpg

Component 概要図

ある要素は、所定始点から終点までの間を 0〜1 の係数として、プロパティに変化を与えられます。今回の例では、window.scrollY = 0px〜200px の間を 1〜0 とし、150px 加算の係数としています。

1208.png

0〜1 の値が得られるので、係数利用で様々な inline-style を構築することが出来ます。Hero image 以外の要素にも適用出来るので遊んでみてください。

なお、ランディングページなどでは派手な演出効果として有効かもしれませんが、アプリケーションを作っている場合、この効果の多様は鬱陶しいだけのものになり兼ねません。目的を明確にし、機能と連動する有益な効果にしましょう。

Context API / Provider 構成

今日はこのカレンダーサンプル集で、初めて Context API を利用します。まず始めに、createContext で空の Context を作成します。サンプルでは TypeScript を利用しているので、assertion を付与します。最新の@types/react で既に hooks の型定義が入っているので、そちらを利用します。(この箇所はカレンダー公開時から変更が入っていますので、現行のリポジトリを参照ください。)

components/context.ts
export const CTX = createContext({} as {
  coefficient: number
  updateCoefficient: Dispatch<SetStateAction<number>>
})

次に、useState の戻り値を Provider の初期値として注入します。

components/provider.tsx
export default (props: Props) => {
  const [coefficient, updateCoefficient] = useState(1)
  return (
    <Provider value={{ coefficient, updateCoefficient }}>
      {props.children}
    </Provider>
  )
}

今回は Custom Hooks を用意せず、Wrapper Component に記述しています。今までと同様に、lodash.throttlelodash.merge を利用します。

components/wrapper.tsx
export default (props: Props) => {
  const { updateCoefficient } = useContext(CTX)
  const options = useMemo(
    (): Options =>
      merge(defaultOptions(), {
        threthold: props.threthold,
        throttleInterval: props.throttleInterval
      }),
    [props.threthold, props.throttleInterval]
  )
  const handleScroll = useCallback(
    throttle(() => {
      const diff = options.threthold - window.scrollY
      const offset = diff > 0 ? diff : 0
      const coefficient = offset / options.threthold
      updateCoefficient(coefficient)
    }, options.throttleInterval),
    [options.threthold, options.throttleInterval]
  )
  useEffect(() => {
    handleScroll()
    window.addEventListener('scroll', handleScroll)
    return () =>
      window.removeEventListener('scroll', handleScroll)
  }, [])
  return (
    <div className={props.className}>{props.children}</div>
  )
}

ここまでで用意したものを組み立てると以下の構成になります。(Head Component に指定している props は、Hooks や Context API とは無関係)

components/index.tsx
const View = (props: Props) => (
  <>
    <Provider>
      <Wrapper className={props.className}>
        <Head
          title={'Entry Title'}
          subTitle={'subTitle'}
          avatorImgSrc={require('../assets/avator.jpg')}
          bgImgSrc={require('../assets/hero.jpg')}
          bgColor={'#004c94'}
        />
      </Wrapper>
    </Provider>
    <Body />
    <Footer />
  </>
)

Container Component

ContextAPI では createContext で同時に Consumer が得られますが、useContext を利用する場合はそれは不要になります。useContext する層は、Redux の connect 相当の働きをするため Container と呼んでいます。接続する context に変化がある度この層は再実行対象になります。下層コンポーネントへの不要な伝搬をブロックするため、ここで memoize します。

components/head/index.tsx
export default (props: ContainerProps) => {
  const { coefficient } = useContext(CTX)
  const isHide = useMemo(() => coefficient < 0.5, [
    coefficient
  ]) // 表示エリアが半分になったらヘッダーに変形させるフラグ
  const nodeStyle = useMemo(
    () => ({ height: 60 + coefficient * 150 }),
    [coefficient]
  )
  const bgStyle = useMemo(
    () => ({
      opacity: coefficient + 0.2,
      transform: `scale(${1 + coefficient * 0.2})`
    }),
    [coefficient]
  )
  return useMemo(
    () => (
      <StyledView
        isHide={isHide}
        bgStyle={bgStyle}
        nodeStyle={nodeStyle}
        {...props}
      />
    ),
    [isHide, nodeStyle, bgStyle]
  )
}

変化が必要なタイミングで props が styled-components を経由し、子コンポーネントへ伝播します。

components/head/index.tsx
const StyledView = styled(View)`...`
components/head/index.tsx
const View = (props: Props) => (
  <div className={props.className}>
    <div className="bg" style={props.bgStyle} />
    <div className="wrapper" style={props.nodeStyle}>
      <Photo
        isHide={props.isHide}
        imgSrc={props.avatorImgSrc}
      />
      <Description
        title={props.title}
        subTitle={props.subTitle}
        isHide={props.isHide}
      />
    </div>
  </div>
)

Child Component

子コンポーネントにあたるサムネイル画像です。description が隠れている状態フラグを「isHide」として受領します。ここでも、styled-components の高階コンポーネントで memoize します。

components/head/photo.tsx
export default (props: ContainerProps) => {
  const nodeStyle = useMemo(
    () => {
      return props.isHide
        ? {
            transform: `scale(.5) translateX(-30px)`
          }
        : { transform: `scale(1) translateX(0px)` }
    },
    [props.isHide]
  )
  return useMemo(
    () => (
      <StyledView
        nodeStyle={nodeStyle}
        imgSrc={props.imgSrc}
      />
    ),
    [nodeStyle]
  )
}

算出した props は StyledView を経由し、FC に到達します。

components/head/photo.tsx
const StyledView = styled(View)`...`
components/head/photo.tsx
const View = (props: Props) => (
  <p className={props.className} style={props.nodeStyle} />
)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1