Design Converter
Education
Last updated on May 8, 2024
•10 mins read
Last updated on May 8, 2024
•10 mins read
When developing maintainable react applications, a fundamental concept that often comes into play is the separation of concerns.
This principle is crucial when we talk about React separate UI and logic.
The challenge of modifying a single component due to the interconnected nature of components in a React application often requires developers to consider the impact on all the components that are coupled with it, to avoid unpredictable side effects.
By decoupling business logic from the user interface (UI), developers can create more modular, testable, and scalable testable apps. This separation not only enhances code reusability but also simplifies the maintenance of the react application.
In React, this typically means separating the visual components (UI components) from the underlying logic that manages the state and data manipulation. This approach aligns with the single responsibility principle, ensuring that each react component or function app has a clear and distinct purpose.
Let’s delve into how this separation can be achieved and why it’s a great idea for building robust applications.
The architecture of React component often involves a pattern where components are categorized as either container components or presentational components. Container components are concerned with how things work—they manage business logic, state management, and api calls. On the other hand, presentational components, also known as UI components, are focused on how things look—they are responsible for rendering the UI based on the data they receive.
1// Example of a container component in React 2class UserContainer extends React.Component { 3 state = { users: [] }; 4 5 componentDidMount() { 6 fetchUsers().then(users => this.setState({ users })); 7 } 8 9 render() { 10 return <UserList users={this.state.users} />; 11 } 12} 13 14// Example of a presentational component in React 15const UserList = ({ users }) => ( 16 <ul> 17 {users.map(user => ( 18 <li key={user.id}>{user.name}</li> 19 ))} 20 </ul> 21);
The single responsibility principle is a design pattern that dictates that a component or module should have one, and only one, reason to change. This principle is a guide for scalable testable applications, ensuring that each component is only tasked with a single aspect of the application's functionality. By adhering to this principle, developers can create separate components that are easier to understand, test, and maintain.
In a react application, container components serve as the ideal location for encapsulating business logic. This is because they act as intermediaries between the data layer and the UI, handling tasks such as data fetching, state updates, and logic operations. By confining the business logic to container components, developers can keep their UI components clean and focused solely on presentation.
1// Example of business logic within a container component 2class UserProfileContainer extends React.Component { 3 state = { user: null, isLoading: true }; 4 5 async componentDidMount() { 6 const user = await fetchUserProfile(this.props.userId); 7 this.setState({ user, isLoading: false }); 8 } 9 10 render() { 11 const { user, isLoading } = this.state; 12 return <UserProfile user={user} isLoading={isLoading} />; 13 } 14}
Custom hooks in React offer a powerful way to extract component logic into reusable functions. By creating custom hooks, developers can separate logic from the components, making it easier to share and manage across different components. Custom hooks encapsulate stateful logic and side effects, which can then be easily utilized by other components without duplicating code.
1// Example of a custom hook for business logic 2function useUserProfile(userId) { 3 const [user, setUser] = useState(null); 4 const [isLoading, setIsLoading] = useState(true); 5 6 useEffect(() => { 7 async function fetchUser() { 8 const userData = await fetchUserProfile(userId); 9 setUser(userData); 10 setIsLoading(false); 11 } 12 13 fetchUser(); 14 }, [userId]); 15 16 return { user, isLoading }; 17}
To illustrate an example of business logic in a React app, let's consider a custom hook that handles data fetching. This hook encapsulates the logic for making API calls, managing loading states, and handling errors. By using this hook, any react component can easily obtain the necessary data without worrying about the intricacies of the fetching process.
1// Example of a custom hook for data fetching 2function useFetchData(url) { 3 const [data, setData] = useState(null); 4 const [loading, setLoading] = useState(true); 5 const [error, setError] = useState(null); 6 7 useEffect(() => { 8 const fetchData = async () => { 9 try { 10 const response = await fetch(url); 11 const result = await response.json(); 12 setData(result); 13 setLoading(false); 14 } catch (err) { 15 setError(err); 16 setLoading(false); 17 } 18 }; 19 20 fetchData(); 21 }, [url]); 22 23 return { data, loading, error }; 24}
This custom hook can then be used by any component that requires data from an API, thus separating business logic from the UI and promoting code reusability.
Separating business logic from UI in React is made simpler with the use of custom hooks. These hooks allow you to abstract the business logic away from the UI components, making the logic portable and reusable across multiple components. For instance, if you have a form component that needs to validate user input, you can create a custom hook that contains all the validation logic.
1// Example of a custom hook for form validation 2function useFormValidation(initialState, validate) { 3 const [values, setValues] = useState(initialState); 4 const [errors, setErrors] = useState({}); 5 6 const handleChange = event => { 7 const { name, value } = event.target; 8 setValues({ ...values, [name]: value }); 9 }; 10 11 const handleBlur = () => { 12 const validationErrors = validate(values); 13 setErrors(validationErrors); 14 }; 15 16 return { values, errors, handleChange, handleBlur }; 17}
Once the business logic is separated into custom hooks, the UI components can be designed purely for presentation. These components, often referred to as "dumb" or "stateless" components, receive data and callbacks as props and are solely responsible for rendering the UI. This clear separation enhances the maintainability of the code and simplifies the testing process.
1// Example of a UI component 2const LoginForm = ({ values, errors, handleChange, handleBlur, handleSubmit }) => ( 3 <form onSubmit={handleSubmit}> 4 <input 5 type="text" 6 name="username" 7 value={values.username} 8 onChange={handleChange} 9 onBlur={handleBlur} 10 /> 11 {errors.username && <p>{errors.username}</p>} 12 <input 13 type="password" 14 name="password" 15 value={values.password} 16 onChange={handleChange} 17 onBlur={handleBlur} 18 /> 19 {errors.password && <p>{errors.password}</p>} 20 <button type="submit">Login</button> 21 </form> 22);
React clean architecture is about structuring your application in a way that separates concerns and promotes maintainable code. It involves organizing your code into layers with clear boundaries, such as presentation, application, and domain layers. This approach helps in decoupling business logic from the UI and other cross-cutting concerns, leading to a more testable and scalable application.
Higher order components (HOCs) and higher order functions are design patterns in React that can help with separating concerns and enhancing code reusability. A higher order component is a function that takes a component and returns a new component, whereas a higher order function is a function that takes a function and returns a new function. Both can be used to abstract shared logic across different components or functions.
1// Example of a higher order component 2function withUserData(WrappedComponent) { 3 return class extends React.Component { 4 state = { user: null }; 5 6 componentDidMount() { 7 fetchUserData().then(user => this.setState({ user })); 8 } 9 10 render() { 11 return <WrappedComponent {...this.props} user={this.state.user} />; 12 } 13 }; 14}
The Context API is a React structure that enables you to share state across the entire application without having to pass props down manually through every level of the component tree. It's particularly useful for managing global data like user authentication, theme settings, or language preferences. This can greatly simplify state management and reduce the complexity of your component hierarchy.
1// Example of using Context API for state management 2const UserContext = React.createContext(); 3 4function UserProvider({ children }) { 5 const [user, setUser] = useState(null); 6 7 // Business logic to fetch and set the user data 8 useEffect(() => { 9 fetchUser().then(setUser); 10 }, []); 11 12 return <UserContext.Provider value={user}>{children}</UserContext.Provider>; 13} 14 15function UserProfile() { 16 const user = useContext(UserContext); 17 return user ? <div>Welcome, {user.name}</div> : <div>Loading...</div>; 18}
Effective file structure and code organization are key to creating maintainable react applications.
Organizing your project's file structure logically is essential for developers to navigate and understand the codebase quickly. A common approach is to group files by feature or route and then further by the nature of the components, such as containers and presentational components. This helps in maintaining a clean architecture, a guide for scalable testable apps.
1// Example of a file structure for a user feature 2src/ 3|-- components/ 4| |-- UserList/ 5| | |-- UserList.js // Presentational component 6| | |-- UserList.css 7| |-- UserProfile/ 8| | |-- UserProfile.js // Presentational component 9| | |-- UserProfile.css 10|-- containers/ 11| |-- UserListContainer.js // Container component 12| |-- UserProfileContainer.js // Container component 13|-- hooks/ 14| |-- useUserData.js // Custom hook for fetching user data 15|-- utils/ 16| |-- api.js // Utility functions for API calls 17|-- App.js 18|-- index.js
Higher order components (HOCs) are also instrumental in handling cross-cutting concerns such as logging, error handling, and analytics. By wrapping components with HOCs, you can inject additional behavior without modifying the original component's logic. This technique promotes code reusability and separation of concerns.
1// Example of a higher order component for error handling 2function withErrorHandling(WrappedComponent) { 3 return class extends React.Component { 4 state = { hasError: false }; 5 6 static getDerivedStateFromError(error) { 7 return { hasError: true }; 8 } 9 10 render() { 11 if (this.state.hasError) { 12 return <div>Something went wrong.</div>; 13 } 14 return <WrappedComponent {...this.props} />; 15 } 16 }; 17}
Component architecture in React is all about how components are organized and work together to build the application. Understanding the hierarchy and how components compose with one another is crucial for creating a scalable and maintainable react application. Components should be designed to be as independent as possible, which allows for easier testing and maintenance.
Separating concerns in component design is not just about separating business logic from UI components; it's also about ensuring that each component has a single responsibility. This separation allows for more focused development and testing of each component, reducing the likelihood of bugs and making the codebase more manageable.
1// Example of a child component with a single responsibility 2const UserNameDisplay = ({ userName }) => <h1>Welcome, {userName}!</h1>;
To build maintainable and scalable React applications, it's essential to separate UI components from business logic, use container components and custom hooks effectively, and apply clean architecture principles. By following these best practices, developers can create applications that are easier to understand, test, and maintain.
I hope you found this article helpful in understanding how to react separate ui and logic for more maintainable react applications. Remember, the key to mastering React is to keep learning and experimenting with different patterns and techniques. Keep coding, keep improving, and as always, happy coding!
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.