App
component code: class App extends Component { state = { users: [{name: 'Jim', surname: 'Smith', age: 33}] }; componentDidMount() { this.fetchUsers(); } async fetchUsers() { const response = await fetch('http://totallyhardcodedurl.com/users'); const users = await response.json(); this.setState({users}); } render() { return ( <div className="App"> <header className="App-header"> // </header> <table> <thead> <tr> <th>First name</th> <th>Last name</th> <th>Age</th> </tr> </thead> <tbody> {this.state.users.map((user, index) => ( <tr key={index}> <td><input value={user.name} onChange={/* update name in the state */}/></td> <td><input value={user.surname} onChange={/* update surname in the state*/}/></td> <td><input value={user.age} onChange={/* update age in the state */}/></td> </tr> ))} </tbody> </table> <button onClick={() => this.saveUsersOnTheBackend()}>Save</button> </div> ); } saveUsersOnTheBackend(row) { fetch('http://totallyhardcodedurl.com/users', { method: "POST", body: JSON.stringify(this.state.users), }) } }
App
component by refactoring it. Its code, after splitting it into several components, will look like this: class App extends Component { render() { return ( <div className="App"> <Header/> <UserList/> </div> ); } }
Header
component, and if we need to add a new component to the application, we change the App
component. Here we have solved problems # 1 (changing the header of the application) and problem # 2 (adding a new component to the application). This is done by moving the corresponding logic from the App
component to the new components.UserList
solve problems №3 and №4, creating the class UserList
. Here is his code: class UserList extends Component { static propTypes = { fetchUsers: PropTypes.func.isRequired, saveUsers: PropTypes.func.isRequired }; state = { users: [{name: 'Jim', surname: 'Smith', age: 33}] }; componentDidMount() { const users = this.props.fetchUsers(); this.setState({users}); } render() { return ( <div> <UserTable users={this.state.users} onUserChange={(user) => this.updateUser(user)}/> <button onClick={() => this.saveUsers()}>Save</button> </div> ); } updateUser(user) { // } saveUsers(row) { this.props.saveUsers(this.state.users); } }
UserList
is our new container component. Thanks to him, we solved problem number 3 (changing the user loading mechanism) by creating the function-properties fetchUser
and saveUser
. As a result, now, when we need to change the link used to load the list of users, we turn to the corresponding function and make changes to it.UserTable
, which encapsulates the formation of HTML-code and styling the table with users.UserList
component described above, you will notice that if you need to display a list of users in a different format, we will have to modify the render
method of this component. This is a violation of the principle of openness-closeness.UserList
component that has been refactored: export class UserList extends Component { static propTypes = { fetchUsers: PropTypes.func.isRequired, saveUsers: PropTypes.func.isRequired }; state = { users: [{id: 1, name: 'Jim', surname: 'Smith', age: 33}] }; componentDidMount() { const users = this.props.fetchUsers(); this.setState({users}); } render() { return ( <div> {this.props.children({ users: this.state.users, saveUsers: this.saveUsers, onUserChange: this.onUserChange })} </div> ); } saveUsers = () => { this.props.saveUsers(this.state.users); }; onUserChange = (user) => { // }; }
UserList
component, as a result of the modification, turned out to be open for expansion, since it displays the child components, which makes it easier to change its behavior. This component is closed for modification, as all changes are performed in separate components. We can even deploy these components independently. export class PopulatedUserList extends Component { render() { return ( <div> <UserList>{ ({users}) => { return <ul> {users.map((user, index) => <li key={index}>{user.id}: {user.name} {user.surname}</li>)} </ul> } } </UserList> </div> ); } }
UserList
component, creating a new component that knows how to display a list of users. We can even download more detailed information about each of the users in this new component, without touching the UserList
component, and this is precisely the purpose of refactoring this component. class User { constructor(roles) { this.roles = roles; } getRoles() { return this.roles; } } class AdminUser extends User {} const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser({role: 'moderator'},{role: 'admin'}); function showUserRoles(user) { const roles = user.getRoles(); roles.forEach((role) => console.log(role)); } showUserRoles(ordinaryUser); showUserRoles(adminUser);
User
, whose constructor assumes user roles. Based on this class, we create the AdminUser
class. After that, we created a simple showUserRoles
function that takes a User
object as a parameter and displays to the console all the roles assigned to the user.ordinaryUser
and adminUser
, and then we encounter an error.AdminUser
class AdminUser
is similar to the User
class object. It definitely "quacks" as User
, since it has the same methods as User
. The problem is the "battery". The fact is that by creating the adminUser
object, we passed it a couple of objects, not an array.showUserRoles
function must work correctly with objects of the User
class and with objects created on the basis of the heir classes of this class.AdminUser
constructor instead of the objects: const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser(['moderator','admin']);
class UserTable extends Component { ... render() { const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33}; return ( <div> ... <UserRow user={user}/> ... </div> ); } ... } class UserRow extends Component { static propTypes = { user: PropTypes.object.isRequired, }; render() { return ( <tr> <td>Id: {this.props.user.id}</td> <td>Name: {this.props.user.name}</td> </tr> ) } }
UserTable
component UserRow
component, passing to it, in properties, an object with complete information about the user. If we analyze the code of the UserRow
component, it turns out that it depends on the object containing all the information about the user, but all he needs is the id
and name
properties.user
object with all its properties, otherwise the compiler will generate an error. class UserTable extends Component { ... render() { const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33}; return ( <div> ... <UserRow id={user.id} name={user.name}/> ... </div> ); } ... } class UserRow extends Component { static propTypes = { id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, }; render() { return ( <tr> <td>Id: {this.props.id}</td> <td>Name: {this.props.name}</td> </tr> ) } }
class App extends Component { ... async fetchUsers() { const users = await fetch('http://totallyhardcodedurl.com/stupid'); this.setState({users}); } ... }
App
component depends on the global function fetch
. If we describe the relationship of these entities in the UML language, we get the following diagram.App
component does not need to know how to load user information. In order to solve this problem, we need to invert the dependencies between the App
component and the fetch
function. Below is a UML diagram illustrating this. class App extends Component { static propTypes = { fetchUsers: PropTypes.func.isRequired, saveUsers: PropTypes.func.isRequired }; ... componentDidMount() { const users = this.props.fetchUsers(); this.setState({users}); } ... }
App
component will not change at all.Source: https://habr.com/ru/post/428079/
All Articles