'Functional Component: Write functions inside or outside the component?

I often wrote functional components following a 'Class architecture' where all my function that concern the component are written inside of it like a method in a class.

For example, I have here a function counterAsFloat that is related to the Counter component. As you see I just wrote it inside of the component:

export default function Counter() {
  const [counter, setCounter] = React.useState(0);

  const counterAsFloat = () => {
    return counter.toFixed(2);
  };

  return (
    <div className="counter">
      <h1>{counterAsFloat()}</h1>
      <button onClick={() => setCounter(counter + 1)}>
        Increment
      </button>
    </div>
  );
}

But actually I could also just declare the function outside the component and use it with a parameter:

const counterAsFloat = (counter) => { 
  return counter.toFixed(2);
};

export default function Counter() {
  const [counter, setCounter] = React.useState(0);

  return (
    <div className="counter">
      <h1>{counterAsFloat(counter)}</h1>
      <button onClick={() => setCounter(counter + 1)}>
        Increment
      </button>
    </div>
  );
}

So are there any pros or cons to write the functions outside the functional component?



Solution 1:[1]

This question is pretty opinion-based but there are few notes that you need to think about.

Declaring the function outside of scope is foremost for readability and reusability.

// Reuse logic in other components
const counterAsFloat = (counter) => { 
  return counter.toFixed(2);
};

// If Counter has complex logic, you sometimes want to compose it
// from functions to make it more readable.
export default function Counter() {
  ...

  return (...);
}

One can argue that the first option is less performant because you declare the function on every render:

export default function Counter() {
  ...

  // declare the function on every render
  const counterAsFloat = () => {
    return counter.toFixed(2);
  };

  return (...);
}

Such case is premature optimization. Check out JavaScript closures performance which relates to this.

Note that in this specific case, inlining the function is much better approach.

export default function Counter() {
  ...
  return (
    <div>
      <h1>{counter.toFixed(2)}</h1>
      ...
    </div>
  );
}

Solution 2:[2]

While you could want to use outside functions for organization or reusability, it stills seems to go against the structure of functional components, at least for one reason: in functional components, states are immutable. So they normally are constants. And while your 2 functions seem to be somewhat similar, they differ greatly, precisely regarding this specific feature. Take for example this code:

const a = 2;
function increment(){
    return ++a;
}
increment();

This is obviously forbidden, you cannot change a constant.

Write it differently:

 const a = 2;
 function increment(a){
    return ++a;
 }
 increment(a);

The last one is allowed. It won't give the result you expect, at least looking at it rapidly, but it'll compile and won't have any runtime error.

Transpose this to your example. Let's say that you begin by wanting to simply output yourt counter with toFixed(2), so you create an outside function. But then afterwards you decide that over 5 you want to reset the counter. So you do this:

const counterAsFloat = (counter) => {
    if(counter > 5){
       counter = 0;
    }
    return counter.toFixed(2);
};

This is going to be allowed, will compile and run. It' won't give the expected result, but it won't be obvious. The inside function could work:

const counterAsFloat = () => {
    if(counter > 5){
       counter = 0;
    }
    return counter.toFixed(2);
};

But because in the inside scope counter is a constant you're going to have a compile error or at least a runtime error. That you can quickly fix by replacing counter = 0; by setCounter(0); which is the proper way to handle this requirement.

So in the end, by staying inside your component, it is clearer what the state values are and you're going to have clearer feedback on forbidden manipulations that may be be less obvious with outside functions.

See example with outside function, it is working but doesn't give you the expected result:

const counterAsFloatOutside = (counter) => {
    if(counter > 5){
        counter = 0;
    }
    return counter.toFixed(2);
 };
  

 function Counter() {
  const [counter, setCounter] = React.useState(0);
  

  return (
    <div className="counter">
      <h1>{counterAsFloatOutside(counter)}</h1>
      <button onClick={() => setCounter(counter + 1)}>
        Increment
      </button>
    </div>
    
  );
}
ReactDOM.render(React.createElement(Counter, null), document.body);
 <script type="text/javascript" src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script type="text/javascript" src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

With the inside function, it's just not working, which in this case, is preferable. Working with any compiling tool will even give you the error upfront, which is a huge advantage:

  

 function Counter() {
  const [counter, setCounter] = React.useState(0);
  const counterAsFloat = () => {
    if(counter > 5){
        counter = 0;
    }
    return counter.toFixed(2);
 };

  return (
    <div className="counter">
      <h1>{counterAsFloat()}</h1>
      <button onClick={() => setCounter(counter + 1)}>
        Increment
      </button>
    </div>
    
  );
}
ReactDOM.render(React.createElement(Counter, null), document.body);
<script type="text/javascript" src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script type="text/javascript" src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 T.J. Crowder
Solution 2 johannchopin