DEV Community

Pavel
Pavel

Posted on

React hooks component & file structures 📦

Hello!

Not long ago I thought that my component and file structures were clean and conscious. That was before the arrival of hooks. My components got harder to read and I couldn't fight the feeling that my structure was not clean enough.

Let's start with my folder structure. It was like this before:

src/
  components/
    MyComponent/
      MyComponent.jsx
      MyComponent.test.js
      styled.js
      index.js
    AnotherComponent/
      ...
Enter fullscreen mode Exit fullscreen mode

Nothing special here. Let's move on.

I will consider only functional components as they are essential for hooks. My basic component might look like this:

import React from 'react';
import PropTypes from 'prop-types';
import AnotherComponent from '../AnotherComponent';
import {Wrapper} from './styled';


const CONSTANT_VALUE = true;

function smallUsefulFunction(arg) {
  // do something
  return arg;
}

MyComponent.propTypes = {
  unambiguousProp: PropTypes.string,
};

MyComponent.defaultProps = {
  unambiguousProp: '',
};

export function MyComponent({unambiguousProp}) {
  const enhancedProp = smallUsefulFunction(unambiguousProp);

  if (CONSTANT_VALUE) {
    return <AnotherComponent />;
  }

  return (
    <Wrapper>
      {enhancedProp}
    </Wrapper>
  );
}
Enter fullscreen mode Exit fullscreen mode

The idea is to keep everything as simple and close to each other
as possible without over-complicating.

Now, with hooks, my component might have the following additions:

import React from 'react';
import PropTypes from 'prop-types';
import AnotherComponent from '../AnotherComponent';
import {Wrapper} from './styled';


const CONSTANT_VALUE = true;

function smallUsefulFunction(arg) {
  // do something
  return arg;
}

MyComponent.propTypes = {
  unambiguousProp: PropTypes.string,
};

MyComponent.defaultProps = {
  unambiguousProp: '',
};

export function MyComponent({unambiguousProp}) {
+ const [stateValue, setStateValue] = React.useState(false);
  const enhancedProp = smallUsefulFunction(unambiguousProp);


+ React.useEffect(() => {
+   // do something
+ }, []);
+
+ const handleWrapperClick = () => {
+   setStateValue(prevValue => !prevValue);
+ };

  if (CONSTANT_VALUE) {
    return <AnotherComponent />;
  }

  return (
    <Wrapper onClick={handleWrapperClick}>
      {enhancedProp}
    </Wrapper>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now let's imagine that this component has an overcomplicated state. The best option here is to useReducer(). This implies the addition of a reducer function and (probably) an initial state variable:

import React from 'react';
import PropTypes from 'prop-types';
import AnotherComponent from '../AnotherComponent';
import {Wrapper} from './styled';


const CONSTANT_VALUE = true;

function smallUsefulFunction(arg) {
  // do something
  return arg;
}

+ const initialState = {
+   firstCase: false,
+   secondCase: false,
+ };
+
+ function reducer(state, action) {
+   switch(action.type) {
+     case 'FIRST':
+       return {...state, firstCase: true};
+     case 'SECOND':
+       return {...state, secondCase: true};
+     default:
+       return state; 
+   }
+ }

MyComponent.propTypes = {
  unambiguousProp: PropTypes.string,
};

MyComponent.defaultProps = {
  unambiguousProp: '',
};

export function MyComponent({unambiguousProp}) {
  const [stateValue, setStateValue] = React.useState(false);
+ const [store, dispatch] = React.useReducer(initialState);
  const enhancedProp = smallUsefulFunction(unambiguousProp);

  React.useEffect(() => {
    // do something here
  }, []);

  const handleWrapperClick = () => {
    setStateValue(prevValue => !prevValue);
  };

  if (CONSTANT_VALUE) {
    return <AnotherComponent />;
  }

  return (
    <Wrapper onClick={handleWrapperClick}>
      {enhancedProp}
    </Wrapper>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now let's assume that this component has its own custom hook that we have decided to extract from function body for simplicity:

import React from 'react';
import PropTypes from 'prop-types';
import AnotherComponent from '../AnotherComponent';
import {Wrapper} from './styled';


const CONSTANT_VALUE = true;

function smallUsefulFunction(arg) {
  // do something
  return arg;
}

const initialState = {
  firstCase: false,
  secondCase: false,
};

function reducer(state, action) {
  switch(action.type) {
    case 'FIRST':
      return {...state, firstCase: true};
    case 'SECOND':
      return {...state, secondCase: true};
    default:
      return state; 
  }
}

+ function useSomething(dependency) {
+   const [something, setSomething] = React.useState(null);
+ 
+   React.useEffect(() => {
+     setSomething('something');
+   }, [dependency]);
+   
+   return something;
+ }

MyComponent.propTypes = {
  unambiguousProp: PropTypes.string,
};

MyComponent.defaultProps = {
  unambiguousProp: '',
};

export function MyComponent({unambiguousProp}) {
  const [stateValue, setStateValue] = React.useState(false);
  const [store, dispatch] = React.useReducer(initialState);
+ const something = useSomething(stateValue);
  const enhancedProp = smallUsefulFunction(unambiguousProp);

  React.useEffect(() => {
    // do something here
  }, []);

  const handleWrapperClick = () => {
    setStateValue(prevValue => !prevValue);
  };

  if (CONSTANT_VALUE) {
    return <AnotherComponent />;
  }

  return (
    <Wrapper onClick={handleWrapperClick}>
      {enhancedProp}
    </Wrapper>
  );
}
Enter fullscreen mode Exit fullscreen mode

At last, let's imagine that this component has a repeatable part of JSX that you want to extract but is too small to be extracted into a separate file:

import React from 'react';
import PropTypes from 'prop-types';
import AnotherComponent from '../AnotherComponent';
import {Wrapper} from './styled';


const CONSTANT_VALUE = true;


+ const RepeatableJSX = () => (
+   <div>
+     I repeat
+   </div>
+ );

function smallUsefulFunction(arg) {
  // do something
  return arg;
}

const initialState = {
  firstCase: false,
  secondCase: false,
};

function reducer(state, action) {
  switch(action.type) {
    case 'FIRST':
      return {...state, firstCase: true};
    case 'SECOND':
      return {...state, secondCase: true};
    default:
      return state; 
  }
}

function useSomething(dependency) {
  const [something, setSomething] = React.useState(null);

  React.useEffect(() => {
    setSomething('something');
  }, [dependency]);

  return something;
}

MyComponent.propTypes = {
  unambiguousProp: PropTypes.string,
};

MyComponent.defaultProps = {
  unambiguousProp: '',
};

export function MyComponent({unambiguousProp}) {
  const [stateValue, setStateValue] = React.useState(false);
  const [store, dispatch] = React.useReducer(initialState); 
  const something = useSomething(stateValue);
  const enhancedProp = smallUsefulFunction(unambiguousProp);

  React.useEffect(() => {
    // do something here
  }, []);

  const handleWrapperClick = () => {
    setStateValue(prevValue => !prevValue);
  };

  if (CONSTANT_VALUE) {
    return <AnotherComponent />;
  }

  return (
    <Wrapper onClick={handleWrapperClick}>
      {enhancedProp}
      <RepeatableJSX />
      <RepeatableJSX />
      <RepeatableJSX />
    </Wrapper>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here is the syntax highlighted end version:

import React from 'react';
import PropTypes from 'prop-types';
import AnotherComponent from '../AnotherComponent';
import {Wrapper} from './styled';


const CONSTANT_VALUE = true;

const RepeatableJSX = () => (
  <div>
    I repeat   
  </div>
);

function smallUsefulFunction(arg) {
  // do something
  return arg;
}

const initialState = {
  firstCase: false,
  secondCase: false,
};

function reducer(state, action) {
  switch(action.type) {
    case 'FIRST':
      return {...state, firstCase: true};
    case 'SECOND':
      return {...state, secondCase: true};
    default:
      return state; 
  }
}

function useSomething(dependency) {
  const [something, setSomething] = React.useState(null);

  React.useEffect(() => {
    setSomething('something');
  }, [dependency]);

  return something;
}

MyComponent.propTypes = {
  unambiguousProp: PropTypes.string,
};

MyComponent.defaultProps = {
  unambiguousProp: '',
};

export function MyComponent({unambiguousProp}) {
  const [stateValue, setStateValue] = React.useState(false);
  const [store, dispatch] = React.useReducer(initialState); 
  const something = useSomething(stateValue);
  const enhancedProp = smallUsefulFunction(unambiguousProp);

  React.useEffect(() => {
    // do something here
  }, []);

  const handleWrapperClick = () => {
    setStateValue(prevValue => !prevValue);
  };

  if (CONSTANT_VALUE) {
    return <AnotherComponent />;
  }

  return (
    <Wrapper onClick={handleWrapperClick}>
      {enhancedProp}
      <RepeatableJSX />
      <RepeatableJSX />
      <RepeatableJSX />
    </Wrapper>
  );
}
Enter fullscreen mode Exit fullscreen mode

It just became harder to read, didn't it? If it still not, then try to imagine how it might look at the real-world application with multiple implementations.

After a little struggle I came up with a solution to extract hooks and reducers into a separate files. It looks like this:

src/
  components/
    MyComponent/
      MyComponent.jsx
      MyComponent.reducers.js
      MyComponent.hooks.js
      MyComponent.test.js
      styled.js
      index.js
Enter fullscreen mode Exit fullscreen mode

However, I may have missed something and there might be a better solution to it.

Please, share your ideas!

Top comments (0)