sanniassin / react-input-mask

Input masking component for React. Made with attention to UX.
MIT License
2.22k stars 258 forks source link

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. #188

Open raphaelpc opened 4 years ago

raphaelpc commented 4 years ago

Hello! I'm creating an application using React 16.8.6 + NextJS 9.0.2. It has a page that contains a Form that uses the component React Input Mask. When i run the application, i get the following warning:

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes. in InputMask (at Form.js:348)

I did not find any reference here about that warning. Do anyone know what would be the best solution to resolve it? I tried forcing the form to render only on the client but it makes the page slow, and i dont really think that would be the best solution.

Thanks in advance!

raphaelpc commented 4 years ago

Importante: i'm using "react-input-mask": "^3.0.0-alpha.0",

raphaelpc commented 4 years ago

If anyone faces the same problem, this was my temporary solution (until the component gets updated (IF it gets updated)...):

/*
 * React currently throws a warning when using useLayoutEffect on the server.
 * To get around it, we can lazily show component with useLayoutEffect.
 * Ref: https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
 */
import React, { useState, useEffect } from 'react'
import ReactInputMask from "react-input-mask";

const InputMask = props => {

    const [showChild, setShowChild] = useState(false);

    // Wait until after client-side hydration to show
    useEffect(() => {
        setShowChild(true);
    }, []);

    if (!showChild) {
        // You can show some kind of placeholder UI here
        return null;
    }

    return (
        <ReactInputMask {...props} />
    )
}

export default InputMask;
redbaron76 commented 3 years ago

In this specific case, the best approach I found (InputMask + Next JS) is to use the dynamic import feature from Next. Basically, just create a single component that renders the InputMask component itself:

// components/InputMask.tsx

import MaskedInput from "react-input-mask";

const InputMask: React.FC<Props> = (props) => (
  <MaskedInput {...props} />
);

export default InputMask;

Then, when you want you want to use it, imports it dynamically without server-side rendering:


const CustomComponent: React.FC<Props> = (props) => {

   const InputMaskNoSSR = useMemo(
    () =>
      dynamic(() => import("components/InputMask"), {
        ssr: false,
      }),
    []
  )

   return (<InputMaskNoSSR {...props} />)
}

Doing this, the InputMask component will be rendered in the client only and all warnings will disappear. Happy coding!

JanderSilv commented 2 years ago

My workaround was to use the component from Material-UI:

import { NoSsr } from '@material-ui/core';

export const MaskedTextField = (props: Props) => {
  const { name, control } = props;
  return (
    <NoSsr>
      <Controller
        name={name}
        control={control}
        render={({ field: { value, onChange } }) => (
          <InputMask
            mask="(0)999 999 99 99"
            maskPlaceholder={null}
            value={value}
            onChange={onChange}
          >
            <MuiTextField variant="outlined" fullWidth {...props} />
          </InputMask>
        )}
      />
    </NoSsr>
  );
}
mbraz commented 2 years ago

@redbaron76's solution worked for me!

mmalomo commented 2 years ago

To avoid installing Material-UI you can use this:

import { Fragment, ReactNode, useEffect, useLayoutEffect, useState } from "react";

type Props = {
  children: ReactNode;
}

function NoSsr(props: Props) {
  const [mountedState, setMountedState] = useState(false);
  const useEnhancedEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
  const defer = false;

  useEnhancedEffect(() => {
    if (!defer) {
      setMountedState(true);
    }
  }, [defer]);

  useEffect(() => {
    if (defer) {
      setMountedState(true);
    }
  }, [defer]);

  // We need the Fragment here to force react-docgen to recognise NoSsr as a component.
  return <Fragment>{mountedState ? props.children : undefined}</Fragment>;
}

export default NoSsr
cbdeveloper commented 1 year ago

This is my solution. I have SEO requirements, so I need some input rendered during SSR. I'm rendering my UI library (Chakra) input on SSR and I switch to react-input-mask on the client. I made them to look visually the same.

import { Input } from '@chakra-ui/react';
import InputMask from 'react-input-mask';

// Component

const [showMask, setShowMask] = useState(false);

useEffect(() => {
    setShowMask(true);
  }, []);

const inputElement = showMask ? (
  <InputMask
    mask="99-99-9999"
    maskPlaceholder="dd-mm-yyyy"
    alwaysShowMask
    onChange={onChange}
    value={value}
  />
) : (
  <Input defaultValue="dd-mm-yyyy" />
);