Calculator: Separate model from React component?

Hi. I’m working on the frontend calculator project using only React with hooks and I find it impossible to resist to temptation to keep the model separate from the component. From what I understand a react component is supposed to encapsulate state, behaviour and look and feel into one unit, but in practice it seems I’d have to sacrifice my pure calculator model to achieve this.

So, is it ok to separate the (stateless) model from the component and or should I go all in and dump the shunting yard algorithm etc. in there, too? Also, is it good practice to store model state in a single immutable object (tokens, currentToken, prevResult, errorState) and manage that with useState?

From what I understand, there is no wrong approach. As long as your performance isn’t crippled, you can do whatever you want and, as long as it works, it’s fine.

I definitely just used an array of buttons and mapped over them to create the calculator’s UI. And the only thing I used the state for was to manage the string of numbers and operators the calculator would output. But I’ve seen other projects where every single button is responsible for it’s own state, and then the parent container itself was responsible for its own state as well.

I would say all components that can be stateless, should be stateless (like containers/layout, etc.). Depending on the architecture, you may want to colocate state to components, or pass data down to components using props.

If you want to use a more global approach to state management then something like useReducer is often better than a bunch of useState. Or you can pick some state management library, like Redux (Redux Toolkit, easy-peasy), Recoil, Zustand, Jotai…the list goes on.

1 Like

Yes, but the React bit is just the UI.

Encapsulating things in components doesn’t mean you should encapsulate the actual business logic. You can and absolutely should write a calculator programme that has nothing to do with React – it doesn’t need React to work, React is just a way of writing UI that allows a user to interact with it.

edit: this isn’t specific to React, this holds true for virtually any program/application. Don’t put the core logic in the UI code. The UI is just how someone interacts with the program

Edit edit: You can do it. And in very small applications it can be easier. But it makes things very much more complicated

1 Like

Thank you for your help. I think the transition from MVC to MVVM confused me a bit.

I’m not concerned with which component should maintain state. It seems obvious that stateful buttons would only complicate things. I was wondering if and to what degree business logic should be part of the component hierarchy.

Thanks for mentioning useReducer, I wasn’t aware of that hook and will definitely try it out. I see the value in state management libraries, but I prefer to learn one framework at a time (given my confusion, that’s probably a good idea :)). I’ll probably try out Redux for my next project.

I guess I’m still slightly confused how to manage state between components and business logic. I used to write stateless models and views in the past, maybe that’s why I’m not always sure how React wants to be used :slight_smile:

@DanCouper I get what you’re saying, but I still think that might be a bit confusing to people new to React seeing as the UI is reacting to state changes.


Let’s take a simple to-do list and the implementation of a completed filter function. The business logic would be the filtering logic on the actual to-do list data structure. The resulting new data structure becomes the new state that React will use to update the UI.

Or a simple utility function, like a date format function. It can be inlined into the JSX and not touch the state at all but just transform it before the data is used by React.

Yeah, I should have clarified a bit, I’ve just phrased what I meant really badly. And business logic is the wrong phrase. Also, any front end app is inherently stateful, so there’s not any getting around it. I sort of inferred what you meant here @lincore , and I should have provided an example. Model is the wrong word, but yes, you should, in this case keep the actual calculator logic separate. It’s not a model, it’s just the core of the application. So, say it’s a single function: the job of React is to get the user input into that function, and to render the output from that function. You can test that function, make sure it does what you expect, completely separate from React.

There are methods provided by the React API that allow you to do things like keep hold of bits of state you need to get the correct input (for example building up, say, strings from a user typing on the calculator keypad), and those, generally, should be as close to the edges of the app as possible – in the components. Moving all that to some model – sometimes it’s a good idea, often it’s not.

What I mean:
a. for input, React provides a way to pass some values to wherever core logic is happening, and provides an opportunity to ensure those values are formatted in some specific way
b. for output, React provides a way to render some output from the core logic in some specific way.
And the benefit of it is that, for output, you just declare you want it to render certain data, and it’ll keep the UI in sync whenever that data updates.

So to use @lasjorg’s examples. For a todo list, normally this involves aping database functions (list all entries, update an entry, add an entry, delete an entry). So something like Redux (or even just using React’s Context API + the useReducer hook directly) – that’s a sensible thing. You get something that acts like a tiny database on the front-end, then you use React to send things to it and read from it and your app updates. It’s a model, I guess? You don’t split that all into individual components, you centralise the logic for it, because it’s much easier to think about it that way. For something like a date formatting function: yeeeesss, you do inline it. But IRL you’d likely either use a library or some set of functions you’ve written – they don’t get inlined, only the calls to them, and the actual functions are separate to React. You don’t really want the logic inlined, because it makes it very difficult to debug if the logic is wrong.

So as an example:

import ReactDOM from "react-dom";
import { useState } from "react";

/**
 * This is the core of the application, this is what
 * I'm talking about. The aim is to get the input
 * into this function in a form it expects.
 */
function calculator(input) {
  let output = eval(input);
  return output;
}

/**
 * This uses React's API to provide something that
 * can be used directly in the components. Could all
 * be inlined, same result.
 */
function useCalculator(initialInput = "") {
  const [input, setInput] = useState(initialInput);
  const [output, setOutput] = useState("");

  const runCalculation = (input) => {
    console.log("running calc with input", input);
    setOutput(calculator(input));
  };

  return {
    input,
    setInput,
    output,
    runCalculation
  };
}

/**
  * And the app itself:
  */
const App = () => {
  const { input, setInput, output, runCalculation} = useCalculator();

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        runCalculation(input);
      }}
    >
      <p>Output: {output} </p>
      <input
        type="text"
        value={input}
        pattern="[\s+-/*0-9]+"
        onChange={(e) => setInput(e.target.value)}
      />
      <button type="submit">=</button>
    </form>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />,  rootElement);

So that’s trivial (and it will just break in most cases, I’ve just written it in a few minutes so it’s rubbish – but it works). It has almost no functionality. And all of that stuff could have been inlined. But you need to handle a lot more functionality than what’s in there. So you can work on either the calculator function, and improve that. Or you can work on the useCalculator hook function, which basically acts as a bridge between the components and the calculator function. Or you can work on the component itself, keeping them as simple as possible, all in isolation (note, it’s an example, not something to copy, was just trying to explain thinking)

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.