Search code examples
javascriptreactjsdesign-patternsarchitecture

React Class Component Separate UI & Business Logic


I am trying to separate UI and Logic in React class component with little bit of js. I know this could be done easily with custom hooks. But I need to do with class components.

I could make some progress, but it doesnot seem efficient, need your input.

App.view.js

import React from "react";
import Header from "../components/Header";
import ListWrapper from "../components/ListWrapper";
import SearchField from "../components/SearchField";
import UserCard from "../components/UserCard";
import AppController from "./App.controller";

class App extends React.Component {
  constructor() {
    super();
    this.state = { users: [], searchValue: "" };
    this.setState = this.setState.bind(this);
  }

  componentDidMount() {
    AppController(this.state, this.setState).fetchUsers();
  }

  render() {
    const filteredUsers = AppController(
      this.state,
      this.setState
    ).handleFilter();

    return (
      <div className="wrapper pb-12 bg-gray-100 mx-auto max-w-7xl">
        <Header heading="🐶 Pets Rolodex" />
        <SearchField
          labelText="Search Pets"
          fieldId="pets-search-field"
          placeholderText="Enter name"
          searchValue={this.state.searchValue}
          handleChange={AppController(this.state, this.setState).handleChange}
          className="w-72"
        />
        <ListWrapper
          listData={filteredUsers}
          ListItem={UserCard}
          ListItemProp="user"
        />
      </div>
    );
  }
}

export default App;

App.controller.js

const AppController = (state, setState) => ({
  // Fetch Users info
  fetchUsers: () => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then(res => res.json())
      .then(data => setState({ users: data }))
      .catch(error => console.error(error));
  },

  // Input Field Handler
  handleChange: event => {
    const { value } = event.target;
    setState({ searchValue: value });
  },

  // Filter Handler
  handleFilter: () => {
    const { searchValue, users } = state;
    if (searchValue === "") {
      return users;
    } else {
      return users.filter(usr =>
        usr.name.toLowerCase().includes(searchValue.toLowerCase())
      );
    }
  }
});

export default AppController;

This works fine. Codesandbox

But the issue is, this instantiates multiple objects from function calls AppController(this.state, this.setState) and this happens on every render(){...} It is like I am creating and destroying function stored in memory.

What I tried

I moved the function call to constructor like below:

  constructor() {
    super();
    this.state = { users: [], searchValue: "" };
    this.setState = this.setState.bind(this);
    this.controller = AppController(this.state, this.setState)
  }

and used this.controller everywhere in view.

But this seems to persist initial state (empty) into function's execution context and all logic functions inside it never see a light of new state.

Thanks in advance for your input :)


Solution

  • You can refactor the AppController to class and pass the component instance to its constructor.

    E.g.

    App.tsx:

    import React from "react";
    import ReactDOM from "react-dom";
    import Controller from "./controller";
    
    class App extends React.Component<any, { users: any[]; searchValue: string }> {
      controller: InstanceType<typeof Controller>;
      constructor(props) {
        super(props);
        this.state = { users: [], searchValue: "" };
        this.controller = new Controller(this);
      }
    
      componentDidMount() {
        this.controller.fetchUsers();
      }
    
      render() {
        const filteredUsers = this.controller.handleFilter();
        return (
          <ul>
            {filteredUsers.map((user) => (
              <li key={user.key}>{user.name}</li>
            ))}
          </ul>
        );
      }
    }
    
    ReactDOM.render(<App />, document.getElementById("container"));
    

    controller.ts:

    class Controller {
      compInstance;
      constructor(compInstance) {
        this.compInstance = compInstance;
      }
      // Fetch Users info
      fetchUsers() {
        fetch("https://jsonplaceholder.typicode.com/users")
          .then((res) => res.json())
          .then((data) => this.compInstance.setState({ users: data }))
          .catch((error) => console.error(error));
      }
    
      // Input Field Handler
      handleChange(event) {
        const { value } = event.target;
        this.compInstance.setState({ searchValue: value });
      }
    
      // Filter Handler
      handleFilter() {
        const { searchValue, users } = this.compInstance.state;
        if (searchValue === "") {
          return users;
        } else {
          return users.filter((usr) =>
            usr.name.toLowerCase().includes(searchValue.toLowerCase())
          );
        }
      }
    }
    
    export default Controller;
    

    codesandbox