Presentational Container Design Pattern
O que é
Presentational Container Component é um padrão de design que consiste em separar a lógica do código de apresentação. Em um contexto React isso funciona dividindo um componente em uma parte responsável apenas pela apresentação dos dados para o usuário, geralmente usando a sintaxe JSX. E a outra parte chamada de Container responsável em carregar e processar os dados antes de passá-los para a UI(interface do usuário). Este padrão nos ajuda a organizar o código e criar componentes reutilizáveis que são mais fáceis de manter e podem ser usados em toda a aplicação. Isso funciona bem com o padrão de Desenvolvimento Orientado a Componente!
Como aplicar
Na maioria das vezes, aplicar esse padrão é dividir componentes complexos em duas partes.
Componentes de Apresentação (Presentation Components)
Estes são responsáveis por como a interface do usuário se parece, geralmente não contém nenhum estado e não sabem como carregar ou atualizar os dados que renderizam. Embora eles não devam armazenar estado, não significa que não possam ter estado, mas quando têm, os estados são relacionados com a UI(interface do usuário).
Componentes de Recipiente (Container Components)
São responsáveis por como as coisas funcionam, ou seja, lidar com a lógica e especificar os dados que os componentes de apresentação irão renderizar. Aqui é o lugar em que você deve armazenar a maioria dos estado, buscar os dados, dispatch actions, e assim por diante.
Exemplo simples
Considere o componente a seguir como um exemplo:
export const MainTodoList = () => {
const dispatch = useDispatch();
const unsortedTodos = useSelector<StoreType, ToDo[]>(
(store) => store.todosReducer.todos
);
const todos = useMemo(
() =>
[...unsortedTodos].sort((a, b) => (a.completed && !b.completed ? -1 : 1)),
[unsortedTodos]
);
const handleCheck = useCallback(
(todo: ToDo) => {
dispatch(doUpdateTodo({ ...todo, completed: !todo.completed }));
},
[dispatch]
);
const handleDelet = useCallback(
(todo: ToDo) => {
dispatch(doDeleteTodo(todo));
},
[dispatch]
);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<button active={todo.completed} onClick={() => handleCheck(todo)}>
<CheckIcon />
</button>
<span>{todo.title}</span>
<button onClick={() => handleDelet(todo)}>
<CloseIcon />
</button>
</li>
))}
</ul>
);
};
É um único componente que lida tanto com manipulação de dados utilizando Flux pattern, quanto com a renderização da interface do usuário para a lista de afazeres.
Essa seria uma forma de dividir o componente em uma parte de apresentação(presentational) e outra de recipiente(container):
Apresentação (Presentational)
export const TodoList = ({ todos, onCheck, onDelete }) => (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<button active={todo.completed} onClick={() => onCheck(todo)}>
<CheckIcon />
</button>
<span>{todo.title}</span>
<button onClick={() => onDelete(todo)}>
<CloseIcon />
</button>
</li>
))}
</ul>
);
O componente de apresentação é livre de estados e chamadas à flux actions. Pode ser fácilmente reutilizado em outras partes da aplicação e testado com ferramentas como Storybook.
Recipiente (Container)
export const MainTodoListContainer = () => {
const dispatch = useDispatch();
const unsortedTodos = useSelector<StoreType, ToDo[]>(
store => store.todosReducer.todos
);
const todos = useMemo(() => [...unsortedTodos].sort(
(a, b) => a.completed && !b.completed ? -1 : 1
), [unsortedTodos]);
const handleCheck = useCallback((todo: ToDo) => {
dispatch(doUpdateTodo({ ...todo, completed: !todo.completed }));
}, [dispatch]);
const handleDelete = useCallback((todo: ToDo) => {
dispatch(doDeleteTodo(todo));
}, [dispatch]);
return (<TodoList todos={todos} onCheck={handleCheck} onDelete={handleDelete} />
};
O recipiente contém toda a lógica e especificidades do processamento dos dados que devem alimentar o componente de apresentação. O mesmo não precisa se preocupar como o dados vão ser mostrados em tela, somente como formatar os dados para o componente de apresentação poder renderizar isso.
Como usamos isso hoje
Muitos dos nossos projetos recentes usam esse padrão de alguma forma, é mais fácil de desenvolver componentes isolados e empregar o fluxo de trabalho Storybook first. O projeto da Phormar é um bom exemplo do uso desse padrão. Nossas telas são divididas em um arquivo index
(Recipiente ou Container) e outro template
(Apresentação ou Presentational). De uma olhada nas métricas index e template por exemplo.
Como aprender mais
Material útil
- Dan Abramov - Presentational and Container Components
- Using Presentational and Container Components with Redux
- Video - React Design Patterns: Presentational and Container Components
Validando conhecimento
- Refatore ou crie um simples projeto trocando componentes complexos por componentes de apresentação(Presentational) e de recipiente(Container).