LoginSignup
3
1

More than 5 years have passed since last update.

先日のサンプルは画面内判定で 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} />
)
3
1
0

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
  3. You can use dark theme
What you can do with signing up
3
1