De Angular para React
Um guia prático com código lado a lado, explicações objetivas e perguntas de entrevista. Pra quem já domina Angular (até v18) e quer se sentir seguro em entrevistas React.
Mudança de mentalidade
Angular é um framework opinativo: router, forms, HTTP, DI, testing, CLI vêm na caixa. React é uma biblioteca de UI. O time escolhe as peças.
input(), output(), model(), viewChild(), contentChild(). Os exemplos deste guia usam a API clássica (decorators) por ser mais difundida. Em entrevista pra vaga já em Angular 17+, demonstrar consciência das duas APIs é diferencial.A stack mais comum em 2025/2026:
- Vite (SPA) ou Next.js (SSR/RSC) no lugar do Angular CLI
- TanStack Query no lugar de HttpClient + RxJS pra server state
- Zustand ou Redux Toolkit no lugar de NgRx
- React Hook Form + Zod no lugar de Reactive Forms
- Tailwind CSS pra estilo utilitário (sem ViewEncapsulation)
- Vitest + React Testing Library no lugar de Karma/Jasmine
Mapa rápido de conceitos
Referência rápida: o equivalente React pra cada conceito Angular que você já conhece.
| Angular | React | Nota |
|---|---|---|
| @Component | function Component() | Função que retorna JSX |
| @Input() | props | Parâmetros da função |
| @Output() + EventEmitter | callback prop | Pai passa função |
| ng-content | children prop | Projeção de conteúdo |
| NgModule | — | Não existe; import direto |
| @ViewChild | useRef | Referência ao DOM |
| ngOnInit | useEffect(…, []) | Roda 1x ao montar |
| ngOnDestroy | useEffect cleanup | return () => {...} |
| ngOnChanges | useEffect(…, [dep]) | Ou derivar no render |
| signal() | useState (+ Compiler) | Reativo no Angular; React faz pull no re-render |
| computed() | useMemo | Valor derivado memoizado |
| Pipes | funções puras | Chamadas no JSX |
| Services + DI | Custom hooks + Context | Sem DI formal |
| RxJS Observables | useState / TanStack Query | RxJS opcional (observable-hooks) |
| NgRx Store | Redux Toolkit / Zustand | RTK ≈ NgRx mais enxuto |
| Reactive Forms | React Hook Form + Zod | Uncontrolled por default |
| Angular Router | React Router / Next.js | Loaders = Resolvers |
| ChangeDetection.OnPush | React.memo() | Shallow compare de props |
| trackBy | key prop | Identidade na lista |
| Angular CLI | Vite / Next.js | Vite só faz scaffold; sem ng g xxx |
Componentes & JSX
No Angular, componente = classe com decorator e template separado. No React, componente = função que retorna JSX (JavaScript XML). Marcação e lógica vivem juntas no mesmo arquivo.
@Component({ selector: 'app-greeting', standalone: true, template: ` <h1 class="title"> Olá, {{ name }} </h1> `, }) export class GreetingComponent { @Input() name!: string; }
type GreetingProps = { name: string; }; export function Greeting({ name }: GreetingProps) { return ( <h1 className="title"> Olá, {name} </h1> ); }
class vira className em JSX. Interpolação é {expr}, não {{ expr }}. Nomes de componentes começam com maiúscula. Sem selector; o nome do import é o "seletor".Props, children & eventos
No Angular: @Input() pra dados de entrada, @Output() + EventEmitter pra eventos, e ng-content pra projeção. No React: tudo é props, inclusive callbacks e children.
@Component({ selector: 'app-card', template: ` <div class="card"> <header> <h2>{{ title }}</h2> <button *ngIf="closable" (click)="closed.emit()"> X </button> </header> <ng-content /> </div> `, }) export class CardComponent { @Input() title!: string; @Input() closable = false; @Output() closed = new EventEmitter(); } // uso: <app-card title="Fatura" [closable]="true" (closed)="onClose()"> <p>Total: R$ 200</p> </app-card>
type CardProps = { title: string; onClose?: () => void; children: React.ReactNode; }; export function Card({ title, onClose, children }: CardProps) { return ( <div className="card"> <header> <h2>{title}</h2> {onClose && ( <button onClick={onClose}> X </button> )} </header> {children} </div> ); } // uso: <Card title="Fatura" onClose={() => console.log('fechou')}> <p>Total: R$ 200</p> </Card>
EventEmitter no React. Eventos são funções callback passadas como props. O pai decide o que fazer. Projeção de conteúdo (ng-content) vira a prop children.Estado com useState
No Angular (clássico) você muta propriedades da classe e Zone.js detecta. Com signals (v16+), usa set() / update(). No React: useState retorna [valor, setter]. Estado é imutável.
import { signal } from '@angular/core'; @Component({ template: ` <p>Valor: {{ count() }}</p> <button (click)="inc()">+1</button> `, }) export class CounterComponent { count = signal(0); inc() { // update funcional this.count.update(c => c + 1); } } // Pra objetos/arrays: .update(o => { ... }) // .mutate() existiu mas foi removido em 17.2+
import { useState } from 'react'; export function Counter() { const [count, setCount] = useState(0); // <>...</> = Fragment: agrupa múltiplos // elementos sem criar wrapper no DOM. return ( <> <p>Valor: {count}</p> <button onClick={() => setCount(c => c + 1) }>+1</button> </> ); } // Estado é IMUTÁVEL. Crie novo: // setUser(prev => ({...prev, age: 31}))
user.age = 31; setUser(user). React compara por referência (Object.is), vê o mesmo objeto e ignora. Sempre crie um novo: setUser(prev => ({...prev, age: 31})).Efeitos com useEffect
No Angular, o lifecycle é distribuído: ngOnInit, ngOnChanges, ngOnDestroy. No React, useEffect faz o papel de todos, variando pelo array de dependências.
export class SearchComponent implements OnInit, OnChanges, OnDestroy { @Input() query!: string; results: Item[] = []; private sub!: Subscription; constructor(private http: HttpClient) {} ngOnInit() { console.log('montou'); } ngOnChanges(ch: SimpleChanges) { if (ch['query']) { this.sub?.unsubscribe(); this.sub = this.http .get<Item[]>(`/api?q=${this.query}`) .subscribe(r => this.results = r); } } ngOnDestroy() { this.sub?.unsubscribe(); } }
import { useState, useEffect } from 'react'; export function Search({ query }: { query: string }) { const [results, setResults] = useState<Item[]>([]); // ngOnInit + ngOnChanges + ngOnDestroy // tudo num bloco só: useEffect(() => { let cancelled = false; fetch(`/api?q=${query}`) .then(r => r.json()) .then(data => { if (!cancelled) setResults(data); }); // cleanup (= ngOnDestroy) return () => { cancelled = true; }; }, [query]); // roda quando query muda // ... }
[] vazio = mount + unmount · [dep] = mount + sempre que dep mudar · sem array = todo render (raramente útil). Pegadinha clássica: em StrictMode dev, useEffect roda duas vezes ao montar (mount → cleanup → mount) pra forçar você a escrever cleanups corretos. Em produção, roda 1x. E pra data fetching, prefira TanStack Query em vez de useEffect.Refs, useMemo & useCallback
// @ViewChild: referência ao DOM @ViewChild('inputEl') inputRef!: ElementRef<HTMLInputElement>; ngAfterViewInit() { this.inputRef.nativeElement.focus(); } // computed(): valor derivado (signals) count = signal(0); double = computed(() => this.count() * 2 ); // ChangeDetection.OnPush // evita re-render se inputs não mudaram @Component({ changeDetection: ChangeDetectionStrategy.OnPush, ... })
// useRef: referência ao DOM const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { inputRef.current?.focus(); }, []); return <input ref={inputRef} />; // useMemo: valor derivado memoizado const [count, setCount] = useState(0); const double = useMemo( () => count * 2, [count] ); // React.memo: equivalente a OnPush const ExpensiveChild = React.memo( ({ data }: { data: Item[] }) => { return <List items={data} />; } ); // só re-renderiza se data (shallow) mudar
Controle de fluxo no template
Angular usa diretivas/blocos (*ngIf, @if, *ngFor, @for). React usa JavaScript puro no JSX.
<!-- Condicional --> @if (isLogged) { <p>Bem-vindo, {{ user.name }}</p> } @else { <p>Faça login</p> } <!-- Lista --> @for (item of items; track item.id) { <app-row [data]="item" /> } <!-- Pipe --> <p>{{ birthday | date:'dd/MM/yyyy' }}</p> <!-- Class condicional --> <div [ngClass]="{ 'active': isActive, 'error': hasError }"></div>
{/* Condicional */} {isLogged ? <p>Bem-vindo, {user.name}</p> : <p>Faça login</p> } {/* Lista. Key é obrigatório */} {items.map(item => ( <Row key={item.id} data={item} /> ))} {/* Função pura em vez de pipe */} <p>{formatDate(birthday)}</p> {/* Class condicional (lib clsx) */} <div className={clsx({ 'active': isActive, 'error': hasError, })}></div>
.map() são os idiomas. A key no map faz o mesmo papel do track no @for.DI & Context
Angular tem Dependency Injection sofisticada com providers hierárquicos. React usa Context API pra compartilhar dados sem prop drilling. É mais leve, com tradeoffs.
@Injectable({ providedIn: 'root' }) export class AuthService { private user$ = new BehaviorSubject< User | null >(null); login(u: User) { this.user$.next(u); } logout() { this.user$.next(null); } getUser() { return this.user$.asObservable(); } } // Componente injeta: constructor(private auth: AuthService) {} ngOnInit() { this.auth.getUser().subscribe( u => this.user = u ); }
type AuthCtx = { user: User | null; login: (u: User) => void; logout: () => void; }; const AuthContext = createContext<AuthCtx | null>(null); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const value = useMemo(() => ({ user, login: (u: User) => setUser(u), logout: () => setUser(null), }), [user]); return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> ); } // Custom hook pra consumir: export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error('fora do Provider'); return ctx; } // Uso no componente: const { user, logout } = useAuth();
Custom Hooks
Qualquer função que começa com use e chama outros hooks é um custom hook. Funciona como Angular services, com estado reativo embutido. No Angular, um service compartilha lógica via DI; no React, um custom hook encapsula a mesma lógica.
@Injectable({ providedIn: 'root' }) export class WindowSizeService { width$ = new BehaviorSubject( window.innerWidth ); constructor() { fromEvent(window, 'resize').pipe( debounceTime(200), map(() => window.innerWidth) ).subscribe(this.width$); } } // uso: constructor(ws: WindowSizeService) { ws.width$.subscribe(w => ...); }
function useWindowWidth(delay = 200) { // initializer LAZY (não roda em SSR) const [width, setWidth] = useState(() => typeof window !== 'undefined' ? window.innerWidth : 0 ); useEffect(() => { let timer: ReturnType<typeof setTimeout>; const handle = () => { clearTimeout(timer); timer = setTimeout( () => setWidth(window.innerWidth), delay ); }; window.addEventListener('resize', handle); return () => { clearTimeout(timer); window.removeEventListener('resize', handle); }; }, [delay]); return width; } // uso direto no componente: const width = useWindowWidth();
useWindowWidth() tem sua própria instância de estado. Pra compartilhar uma instância global (singleton), use Context ou Zustand. No Angular, services providedIn: 'root' já são singletons por natureza.Forms
Angular Reactive Forms: FormGroup, FormControl, validators. React: React Hook Form + Zod é a combinação dominante hoje.
// no componente: form = new FormGroup({ email: new FormControl('', [ Validators.required, Validators.email, ]), password: new FormControl('', [ Validators.required, Validators.minLength(8), ]), }); onSubmit() { if (this.form.valid) { this.auth.login(this.form.value); } } <!-- template --> <form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="email" /> <span *ngIf="form.get('email')?.errors ?.['email']"> Email inválido </span> <input type="password" formControlName="password" /> <button [disabled]="form.invalid"> Entrar </button> </form>
import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const schema = z.object({ email: z.string().email('Email inválido'), password: z.string() .min(8, 'Mínimo 8 chars'), }); type Form = z.infer<typeof schema>; export function LoginForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm<Form>({ resolver: zodResolver(schema) }); return ( <form onSubmit={handleSubmit( data => auth.login(data) )}> <input {...register('email')} /> {errors.email && <span>{errors.email.message}</span>} <input type="password" {...register('password')} /> <button disabled={isSubmitting}> Entrar </button> </form> ); }
z.infer<typeof schema> elimina duplicação tipo ↔ validator.Routing
// app.routes.ts export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver }, }, { path: 'admin', canActivate: [authGuard], loadChildren: () => import('./admin/admin.routes') }, ]; <!-- template --> <a routerLink="/users/42">User</a> <router-outlet /> // no componente: const id = this.route .snapshot.params['id'];
// router.tsx const router = createBrowserRouter([ { path: '/', element: <RootLayout />, children: [ { index: true, element: <Home /> }, { path: 'users/:id', element: <UserDetail />, loader: async ({ params }) => fetch(`/api/users/${params.id}`), }, { path: 'admin', lazy: () => import('./admin/Admin') .then(m => ({ Component: m.default })), }, ], }, ]); // componentes usam hooks: const { id } = useParams(); <Link to="/users/42">User</Link> <Outlet /> // = router-outlet
Data Fetching — TanStack Query
No Angular: HttpClient + RxJS + AsyncPipe. No React de hoje: TanStack Query (cache automático, refetch, retry, devtools).
// service @Injectable({ providedIn: 'root' }) export class UserService { constructor(private http: HttpClient) {} getUser(id: string) { return this.http .get<User>(`/api/users/${id}`); } } // componente user$ = this.userService.getUser(id); <!-- template --> <div *ngIf="user$ | async as user"> {{ user.name }} </div> // Nada out-of-the-box: cache via interceptor, // retry com retry() do RxJS, refetch manual. // Ou: NgRx Data, TanStack Query Angular.
// custom hook function useUser(id: string) { return useQuery({ queryKey: ['user', id], queryFn: () => fetch(`/api/users/${id}`) .then(r => r.json()), staleTime: 60_000, // 1min }); } // componente const { data, isLoading, error } = useUser(id); if (isLoading) return <Spinner />; if (error) return <Error />; return <div>{data.name}</div>; // Cache, refetch on focus, retry, // devtools. Tudo incluído.
State Management Global
Você já conhece NgRx. No React, as equivalências diretas são Redux Toolkit (mesmo paradigma) e Zustand (alternativa minimalista que dominou o mercado).
// actions export const inc = createAction('[Counter] Inc'); export const set = createAction( '[Counter] Set', props<{ value: number }>() ); // reducer export const counterReducer = createReducer( { value: 0 }, on(inc, s => ({ value: s.value + 1 })), on(set, (_, { value }) => ({ value })), ); // componente count$ = this.store.select('counter'); inc() { this.store.dispatch(inc()); }
// createSlice = actions + reducer juntos const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { inc: (state) => { state.value += 1; // Immer: "mutação" }, set: (state, action) => { state.value = action.payload; }, }, }); export const { inc, set } = counterSlice.actions; // componente const count = useSelector( (s: RootState) => s.counter.value ); const dispatch = useDispatch(); dispatch(inc());
state.value += 1 e internamente vira atualização imutável. Mesmo conceito do NgRx, com ~50% menos boilerplate.Zustand — a alternativa minimalista
import { create } from 'zustand'; type AuthStore = { user: User | null; login: (u: User) => void; logout: () => void; }; export const useAuth = create<AuthStore>((set) => ({ user: null, login: (user) => set({ user }), logout: () => set({ user: null }), })); // uso (subscrição granular por seletor): const user = useAuth(s => s.user); const logout = useAuth(s => s.logout);
Next.js & Server Components
Next.js é o framework full-stack dominante no ecossistema React. O App Router introduziu Server Components (RSC), conceito que não existe no Angular e cai com frequência em entrevistas.
// server-side rendering via Angular Universal // O componente é o mesmo: roda no // server E no client (hidratação) @Component({ template: ` <h1>{{ user.name }}</h1> <app-edit-button [userId]="user.id" /> `, }) export class UserPage implements OnInit { user!: User; constructor( private http: HttpClient, private route: ActivatedRoute ) {} ngOnInit() { const id = this.route.snapshot.params['id']; this.http.get<User>(`/api/${id}`) .subscribe(u => this.user = u); } } // Todo JS do componente vai pro bundle // do client.
// app/users/[id]/page.tsx // Server Component (DEFAULT no App Router) // Roda SÓ no servidor. Zero JS DO COMPONENTE // no bundle (vai um payload serializado). type Props = { params: { id: string } }; export default async function UserPage({ params }: Props) { // await direto no corpo. Sem hook. const user = await fetch( `${process.env.API}/users/${params.id}`, { next: { revalidate: 60 } } ).then(r => r.json()); return ( <> <h1>{user.name}</h1> <EditButton userId={user.id} /> </> ); } // EditButton.tsx (precisa de interatividade) 'use client'; export function EditButton({ userId }: ...) { const [open, setOpen] = useState(false); // ... }
Armadilhas clássicas
As pegadinhas que mais pegam quem vem de Angular. Entender cada uma é diferencial em entrevista.
Mutação de estado
user.age = 31; setUser(user). React não vê mudança (mesma referência). Sempre crie um novo objeto: setUser(prev => ({...prev, age: 31}))
Stale closure
Função capturada em useEffect com [] vazio referencia valor congelado do mount. Use forma funcional do setter ou inclua dependências.
useEffect infinito
useEffect(() => setData({...data}), [data]). Muda data, que dispara o efeito, que muda data... loop infinito.
key em listas
Esquecer key no .map() ou usar índice como key em listas que reordenam. Equivalente ao trackBy do Angular. Use ID estável.
Context re-renderiza tudo
Se value={{x, y}} é objeto literal, todo consumidor re-renderiza a cada render do Provider. Memoize o value ou use Zustand.
Two-way binding não existe
React é unidirecional: value={x} onChange={e => setX(e.target.value)}. Não tem ngModel. Explicar isso em entrevista é clássico.
useEffect pra derivar estado
useEffect(() => setUpper(name.toUpperCase()), [name]). Errado. Derive direto: const upper = name.toUpperCase(). Re-render é barato.
Inline function quebrando memo
<Child onClick={() => doThing()} />. Função nova a cada render invalida React.memo do Child. Use useCallback quando relevante.
Perguntas de entrevista
Fundamentos
_jsx() de react/jsx-runtime (antes era React.createElement; por isso hoje você não precisa mais import React from 'react'). Não é HTML: usa camelCase (className, onClick, htmlFor), interpola com {}, é expressão JavaScript.key ajuda a identificar elementos em listas.value={x}). Não-controlado: DOM mantém o valor, acessa por ref. React Hook Form usa não-controlado por performance.Hooks
if, loop ou função aninhada. (2) Chamar só de componentes React ou custom hooks. Motivo: React identifica hooks pela ordem de chamada.useMemo(fn, deps) memoiza valor. useCallback(fn, deps) memoiza função (é useMemo(() => fn, deps)). React.memo(Component) memoiza o componente: só re-renderiza se props (shallow compare) mudaram.useEffect com deps vazias. Soluções: incluir deps corretas, usar forma funcional do setter (setCount(c => c+1)), ou guardar valor em useRef.Estado & dados
Server Components & Next.js
await no corpo. Não pode usar hooks de state/efeito. Alivia bundle e aproxima UI do dado.'use client' no topo. Server Component é default no App Router.Performance
useTransition (updates não urgentes), useDeferredValue, Suspense pra data fetching, automatic batching.Roteiro de estudo
Plano pra ~2 semanas leves, focado em te deixar confiante pra entrevista.
Dia 3. Hooks essenciais na prática: implemente
useDebounce, useLocalStorage, usePrevious.Dia 4. TanStack Query + React Router: mini-app consumindo JSONPlaceholder com rota dinâmica.
Dia 5. Reescreva um form real seu do Angular em React Hook Form + Zod.
Dia 6. Pegue um trecho de NgRx e faça equivalente em Zustand e Redux Toolkit. Compare.
Dia 7. Next.js App Router: tutorial oficial, uma página Server Component buscando dados.
Dia 8–9. React DevTools Profiler. Entenda re-renders. Suspense e
useTransition.Dia 10. Releia as perguntas de entrevista. Escreva respostas em voz alta. Mock interview se possível.
Kickoff rápido
# SPA com Vite npm create vite@latest meu-app -- --template react-ts # Full-stack com Next.js npx create-next-app@latest meu-app --typescript --app # Libs recomendadas npm i @tanstack/react-query react-router-dom react-hook-form zod @hookform/resolvers zustand npm i -D @testing-library/react vitest