Como desacoplar a Navegação no React Native

marlonbelomarques

Marlon Marques

Posted on July 17, 2022

Como desacoplar a Navegação no React Native

Quando começamos a desenvolver uma aplicação em React Native, umas das coisas primordiais que todo aplicativo tem é a navegação entre telas. Bem, é comum pensarmos que quando falamos em navegação no React Native, já associamos ao React Navigation e já saímos instalando as bibliotecas e criando a estrutura de navegação. Até ai tudo bem, mas o meu ponto aqui é, quando começamos a injetar diretamente as ações de navegação no nosso Presenter.

Esse questionamento se veio de um incômodo pessoal e dúvida, se existem outras bibliotecas que realizam a navegação no React Native, e sim, existem algumas como React Native Navigation by Wix, React Router Native etc.

Então, e se um dia eu desejar utilizar outra biblioteca de navegação sem ser a do React Navigation, o quanto de esforço eu teria pra fazer isso, considerando que as ações de navegação estão acopladas diretamente no meu Presenter?

Para isso, decidi realizar esse experimento em um projeto pessoal utilizando conceitos como TDD e Clean Architecture, onde você pode encontrá-lo aqui. E aproveitei para escrever este artigo.

Esse artigo está divido nas seguintes etapas:

1 🧐. Realmente faz sentido desacoplar a Navegação no React Native?

2 🤓. Montando a estrutura inicial de Navegação com React Navigation

3 😎. Utilizando Camadas para desacoplar o React Navigation da minha Aplicação

4 🤌🏻. Chamando a Ação de Navigate no nosso Presenter

5 🧑🏻‍💻. Escrevendo alguns cenários de Testes Unitários

🧐 Realmente faz sentido desacoplar a Navegação no React Native?

Quando pensamos no trabalho a mais que isso pode trazer no nosso desenvolvimento, pode não fazer sentido desacoplar as ações de navegação do nosso Presenter. Afinal pode existir algum cenário onde eu abra mão do React Navigation para usar outra biblioteca?

Nesse caso, eu acredito que vai depender da decisão do dev, ou do time em conjunto.

Mas se isso um dia acontecer, imagine o trabalho que ia dar trocar em todos os Presenter's a ação de navegar por exemplo 😵‍💫.

Abaixo podemos ver uma comparação utilizando uma Interface de navegação via props, e a forma comum que é utilizar a navegação direta do React Navigation.

type Props = {
  navigate: Navigate;
};

const WelcomePresenter: React.FC<Props> = ({ navigate }) => {
  const buttonAction = () => {
    navigate.navigateToMyPlans();
  };
}
Enter fullscreen mode Exit fullscreen mode
const WelcomePresenter: React.FC<Props> = ({ navigation }) => {
  const buttonAction = () => {
    navigation.navigate(Routes.ACTIVITY);
  };
}
Enter fullscreen mode Exit fullscreen mode

Aparentemente a diferença é quase nula, mas se pararmos para pensar, quando utilizamos o navigation diretamente, criamos uma dependência da navegação que estamos usando.

🤓 Montando a estrutura inicial de Navegação com React Navigation

Então vamos lá, precisamos primeiro deixar o nosso React Navigation configurado. Considerando que você já saiba instalar as bibliotecas necessárias do React Navigation, irei apenas deixar o link para documentação em casos de dúvida.

Primeiro vamos montar o nosso Container de Navegação:

type Props = {
  setNavigationTop: (navigatorRef: NavigationContainerRef<any>) => void;
  initialRouteName: keyof StackParams;
};

const Navigation: React.FC<Props> = ({
  setNavigationTop,
  initialRouteName,
}) => {
  return (
    <NavigationContainer ref={setNavigationTop} theme={DefaultThemes}>
      <StackNavigation initialRouteName={initialRouteName} />
    </NavigationContainer>
  );
};

export default Navigation;
Enter fullscreen mode Exit fullscreen mode

No projeto eu utilizei o tipo de navegação em pilha, então agora vamos configurar a navegação em pilha das nossas telas:

const Stack = createNativeStackNavigator<StackParams>();

type StackNavigationParams = {
  initialRouteName: keyof StackParams;
};

const StackNavigation: React.FC<StackNavigationParams> = ({
  initialRouteName,
}) => {
  return (
    <Stack.Navigator
      initialRouteName={initialRouteName}
      screenOptions={{
        headerTransparent: true,
        headerBackTitleVisible: false,
        headerTintColor: colors.white,
        title: '',
      }}
    >
      <Stack.Screen name={Routes.WELCOME}>
        {(props) => <WelcomeFactory {...props} />}
      </Stack.Screen>
    </Stack.Navigator>
  );
};

export default StackNavigation;
Enter fullscreen mode Exit fullscreen mode

O que falta agora é só adicionarmos o nosso Navigation na raiz da aplicação, então ficaria mais ou menos assim:

type Props = {
  initialRouteName: keyof StackParams;
};

const Main: React.FC<Props> = ({ initialRouteName }) => {
  return (
    <WrapperScreen>
      <StatusBar barStyle="dark-content" />
      <Navigation
        setNavigationTop={(navigationRef: NavigationContainerRef<any>) =>
          setTopLevelNavigator(navigationRef)
        }
        initialRouteName={initialRouteName}
      />
    </WrapperScreen>
  );
};
Enter fullscreen mode Exit fullscreen mode

Legal, essa seria a configuração necessária para termos em nossa aplicação a navegação entre telas. Obviamente não detalhei tanto as importações e os tipos utilizados, até porque não é objetivo deste artigo. Mas se quiser ver os detalhes, você pode encontrar nesse repositório.

😎 Utilizando Camadas para desacoplar o React Navigation da minha Aplicação

Então finalmente chegamos na cereja 🍒 do bolo, aqui vamos quebrar um pouco o rito, e vamos utilizar alguns conceitos como Camadas, Interfaces, Adapters etc.

Para representar esse desacoplamento da ação de navegação, vamos seguir esse diagrama que demonstra as camadas que vamos utilizar.

Diagram of Navigate

Mas antes de começarmos, vou explicar rapidamente o diagrama acima, a proposta é desacoplarmos da nossa aplicação dependências externas, como por exemplo o React Navigation, para isso utilizaremos um Adapter. Outro ponto interessante é a camada de Domínio, onde nossa camada de Presentation não conhece nada da camada de Data e Infra, a comunicação entre essas camadas se dá através da camada de Domínio, será ela que utilizaremos no nosso Presenter para realizar as navegações.

Começando pelo Domínio:

export interface Navigate {
  navigateToMyPlans(params?: RouteParams): void;
}
Enter fullscreen mode Exit fullscreen mode

Em direção ao Data:

export class NavigateScreenMyPlans implements Navigate {
  constructor(readonly navigateScreen: NavigateScreen) {}

  navigateToMyPlans(params?: RouteParams | undefined): void {
    this.navigateScreen.navigate(Routes.ACTIVITY, params);
  }
}
Enter fullscreen mode Exit fullscreen mode
export interface NavigateScreen {
  navigate(routeName: string, params?: GenericObject | undefined): void;
}
Enter fullscreen mode Exit fullscreen mode

E somente agora utilizaremos as ações do React Navigation na camada de Infra:

export class ReactNavigationAdapter implements NavigateScreen {
  constructor(readonly navigation: NavigationContainerRef<any>) {}
  navigate(routeName: string, params: GenericObject | undefined): void {
    this.navigation.dispatch(
      CommonActions.navigate({ name: routeName, params: params }),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

🤌🏻 Chamando a Ação de Navigate no nosso Presenter

Ótimo, agora que o Core da nossa aplicação está pronta. Agora vem a parte mais simples, que é realizarmos a ação de navegar através da nossa camada de Dominio que será passada via props.

type Props = {
  navigate: Navigate;
};

const WelcomePresenter: React.FC<Props> = ({ navigate }) => {
  const [toggleEnabled, componentsToggle] = useState(false);

  const buttonAction = () => {
    navigate.navigateToMyPlans();
  };

  return (
    <Welcome
      buttonAction={buttonAction}
      componentsToggle={componentsToggle}
      toggleEnabled={toggleEnabled}
    />
  );
};

export default WelcomePresenter;
Enter fullscreen mode Exit fullscreen mode

Então se pararmos para observar, em nenhum momento no nosso Presenter, sabemos que estamos utilizando o React Navigation para realizar a navegação. O que estamos utilizando é apenas uma Interface que é passada via props.

Mas em que momento passamos via props? Para isso vamos utilizar um Factory onde nele faremos a composição das dependências necessárias.

type Props = {
  route: RouteProp<StackParams, Routes>;
  navigation: any;
};

const WelcomeFactory: React.FC<Props> = () => {
  const navigate = useNavigate();
  const navigateScreen = new NavigateScreenMyPlans(navigate);
  return <Welcome navigate={navigateScreen} />;
};

export default WelcomeFactory;
Enter fullscreen mode Exit fullscreen mode

🧑🏻‍💻 Escrevendo alguns cenários de Testes Unitários

Por ultimo, mas não menos importante. Vamos ver como se torna mais fácil e claro, os testes unitários envolvendo a navegação.

Como foi utilizado o TDD, todas as camadas envolvendo a navegação possuem testes unitários, mas como o foco aqui é o desacoplamento do React Navigation do nosso Presenter, então vou mostrar somente o teste feito no nosso Presenter.

describe('Presentation: Welcome', () => {
  test('should navigate with success when button press', () => {
    const { sut, navigateToMyPlansSpy } = makeSut();
    const button = sut.getByTestId('button_id');

    fireEvent.press(button);
    expect(navigateToMyPlansSpy).toHaveBeenCalledTimes(1);
  });
});

const makeSut = () => {
  let navigation = {} as NavigationContainerRef<any>;

  render(
    <Navigation
      setNavigationTop={(navigationRef) => (navigation = navigationRef)}
      initialRouteName={Routes.WELCOME}
    />,
  );

  const navigate = new NavigateScreenMyPlans(navigation);

  const navigateToMyPlansSpy = jest.spyOn(navigate, 'navigateToMyPlans');

  const sut = render(<Welcome navigate={navigate} />);
  return { sut, navigateToMyPlansSpy };
};
Enter fullscreen mode Exit fullscreen mode

Como foi feito todo o desacoplamento da navegação, não precisamos mais testar toda a aplicação até chegar no objetivo do teste. Basta criar a navegação e passar o navigate via props.

Bem, chegamos ao final do artigo, queria compartilhar essa ideia com vocês, onde mostro como podemos remover essa dependência direta do React Navigation dos nossos Presenter's e assim termos maior liberdade e facilidade para um dia trocarmos de bibliotecas de navegação sem grandes problemas, além de deixar o nosso Presenter menos acoplado.

Valeu galera, até a próxima.

💖 💪 🙅 🚩
marlonbelomarques
Marlon Marques

Posted on July 17, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related