15-04-2020 | Programowanie
HOC i Render Props w React
Tagi: 

Dzisiejszy wpis jest z uniwersum React’a, w którym na co dzień programuję. Dotyczy pewnej koncepcji, wzorca programistycznego. Ogólnie chodzi o to aby developer był w stanie tworzyć reużywalne fragmenty kodu, które bez problemu będzie mógł przekazać do nowych komponentów. Koncepcja HOC wiąże się także z tym, że w Reactcie mamy dwa rodzaje komponentów: mądre komponenty (np HOC) które zawierają logikę i takie które służą tylko do renderowania fragmentów interfejsu. Jednakże zdarza się że taki prosty komponent potrzebuję mieć w sobie jakąś logikę / funkcjonalność, mało tego, okazuje się że mamy trzy takie komponenty które potrzebują mieć tą samą logikę. Nie będę przecież pisał trzy razy tych samych funkcji. A co moge zrobić ? no właśnie … i o tym jest ten wpis. Uff jak już przebrnąłem przez wstep to teraz na konkretnym przykładzie pokażę czym są HOC i render props, oraz jak w ogóle z nimi można pracować.

HOC

Definicyjnie HOC ( Higher Order Components ) czyli Komponent Wyższego Rzędu, jest to funkcja przekształcająca jeden komponent w inny komponent. Można to powiedzieć, że HOC opakowuje komponent nadając mu dodatkowe właściwości, wzbogacając, upiększającego. Myślę że taka forma definicji pozwala już się zorientować co się tutaj będzie odbywało 🙂

Na początek jak zawsze prosty przykład:

Tworzę HOC

export default WrapperApp = (Component) => {
	return (props) => (
   		<Component {...props} />
 	);
};

i potem używam go w celu opakowania komponentu

const App = (props) => {
	return (
   		<span>Hello World!!</span>
 	);
}
export default WrapperApp(App);

i gotowe. Cała koncepcja HOC w pigułce i pozamiatane. Oczywiście bardzo często konkretny przypadek użycia jest o wiele bardziej skomplikowany niż prosty przykład.

Dlatego rozszerze trochę teraz najprostszy przypadek do odrobinę bardziej skomplikowanej historii.

Pisałem sobie aplikację, na samym początku chciałem zrobić panel logowania wraz z potrzebnymi funkcjami, aby dostęp do dalszej części systemu był zablokowany dla osób niezarejestrowanych itd itd. Pomyślałem, porysowałem w Sketchu i wymyśliłem jak będzie wyglądał mój panel logowania. Ogólnie interfejs bogaty w interakcje z użytkownikiem, dlatego jako panel logowania nie wystarczył mi zwykły form. Więc zbudowałem komponent panelu logowania , rejestracji oraz przypomnienia hasła. Skończone, formater, git commit  i puuush? Ale czy aby na pewno? Przyglądam się temu co napisałem i co widzę.

Mam stronę logowania, rejestracji oraz przypomnienie hasła. Są to trzy komponenty.  W związku z interakcjami na interfejsie potrzebowałem rozszerzyć działanie `onFocus` oraz `onBlur`. De facto każdy z tych komponentów zawiera takie same metody.

LoginPage.js

const LoginPage = props => {
	const onFocus = field => {
  		// działania na focusie
 	};
 	const onBlur = field => {
  		// działania na blur
 	};
  	return (
  		<LoginForm/>
  	)
}
export default LoginPage

RegisterPage.js

const RegisterPage = props => {
	const onFocus = field => {
  		// działania na focusie
 	};
 	const onBlur = field => {
  		// działania na blur
 	};
	return (
		<RegisterForm/>
	)
}
export default RegisterPage

oraz ForgotPage.js

const ForgotPage = props => {
	const onFocus = field => {
  		// działania na focusie
 	};
 	const onBlur = field => {
  	// działania na blur
 	};
	return (
		<ForgotForm/>
	)
}
export default ForgotPage

Jak widać w każdym komponencie potrzebne są te same funkcje, oczywiście jak coś działa to jest dobre, a to rozwiązanie działa, lecz czy jest optymalne? Oczywiście że nie, inaczej by tego wpisu nie było 🙂 . Dobra, mówiąc szczerze to absurdalne aby te funkcje musiały być powtarzane w każdym komponencie. Pora w takim razie jakoś to rozwiązać.

Mój zamysł był taki, aby utworzyć HOC, do którego przeniosę funkcje onFocus oraz onBlur, takie opakowanie posłuży do obsłużenia wszystkich komponentów. Mam więc komponent formWrapper.js

export default (Component) => {
	const onFocus = field => {
 		// działania na focus
	};
 	const onBlur = field => {
 		// działania na focus
	};
 	return (props) => (
    	<Component {...props} onFocus={onFocus} onBlur={onBlur}/>
 	);
};

następnie używam tego komponentu w LoginPage.js

const LoginPage = props => {
	return (
		<LoginForm/>
	)
}
export default formWrapper(LoginPage)

analogicznie postępuje z komponentami RegisterPage oraz ForgotPage. Logika obsługi onFocus i onBlur znajduje się w jednym komponencie formWrapper , jest ona przekazywana do komponentu potomnego. Łatwe i wygodne do jakichkolwiek modyfikacji. Minusem jest konieczność stworzenia templatki dla HOC oraz sposób użycia, mam na myśli to:

export default formWrapper(LoginPage)

ponieważ dopiero na końcu widać że został użyty HOC. Komponent opakowany poprzez HOC otrzymuje wszystkie jego właściwości, nie ma też nad tym za dużej kontroli. Do tematu można też podejść trochę inaczej, wykorzystując Render Props.

Render Props

Render Props są pewnym rozszerzeniem wzorca HOC. Generalnie, koncepcja ta, podobnie zresztę  jak HOC, zakłada udostępnianie reużywalnej logiki, lecz developer ma nad tym o wiele lepszą kontrolę i jest bardziej elastyczne. Render Props swoją nazwę zawdzięcza temu że przekazujemy w propsie `render`  komponentu, przekazując funkcję która ma się wykonać i zwraca jej wynik. Na prostym przykładzie wygląda to tak:

const App = props => {
	return props.render();
};
 
const HelloWord = () => {
	return "Hello world!";
};
<App render={() => {HelloWord}} />;

Mogę założyć, że elementem który chcę wyświetlić będzie children komponentu, wtedy ogólny koncept będzie miał taką formę:

const App = props => {
	return props.children();
};
const HelloWord = () => {
	return <div>Hello world!</div>;
};
<App>
	{() => <HelloWord/>}
</App>

Pomyślałem, że do mojego przykładu panelu logowania, to podejście będzie w sam raz. No to do zabrałem się do roboty. Chciałem przekształcić swój kod tak aby korzystał z render props.

Stworzyłem komponent z logiką obsługi formularza.

export const FormWrapper = props => {
	const onFocus = name => {;
   	//działanie na focus
 	};
 
	const onBlur = name => {
   		// działanie na blur
 	};
 
 	const newProps = {
     ...props,
     onFocus,
     onBlur,
   	}
	return props.children({...newProps})
 };
 
export default FormWrapper;

I użyłem go w LoginPage

const LoginPage = props => {
  return (
     <FormWrapper>
         {(props) => {return <LoginForm {...props}/>}}
     </FormWrapper>
  );
}

analogicznie w RegisterPage

const RegisterPage = props => {
  return (
     <FormWrapper>
         {(props) => {return <RegisterForm {...props}/>}}
     </FormWrapper>
   );
}

oraz w ForgotPage

const ForgotPage = props => {
  return (
     <FormWrapper>
         {(props) => {return <ForgotForm {...props}/>}}
     </FormWrapper>
  );
}

I gotowe. Kod stał się  o wiele bardziej przejrzysty. Dużą zaletą jest elastyczność tego rozwiązania. Jeżeli na przykład w LoginPage potrzebuję dodać funkcjonalność, która niekoniecznie potrzebna jest w RegisterPage. Nie muszę dodać jej do FormWrapper, mogę natomiast przekazać je jako parametr

const LoginPage = props => {
  const validation = () => {
   	// działania funkcji
  }
  return (
     <FormWrapper validation={validation}>
         {(props) => {return <LoginForm {...props}/>}}
     </FormWrapper>
  );
}

Podsumowanie

Obsługa formularza jest w jednym miejscu, mogę swobodnie udostępniać ją na wszystkie formularze, w razie potrzeby mogę także dodać dodatkowe funkcje. Generalnie, o to mi właśnie chodziło. W moim przypadku HOC zadziałał poprawnie, lecz lepiej się sprawdziło wykorzystanie Render Props. Wyczytałem gdzieś, że Render Props wprowadzono aby zastąpiły HOC i może coś w tym jest.  Dla mnie na pewno Render Props jest przyjemniejsze i wygodniejsze w stosowaniu, daje o wiele większą elastyczność.

To tyle na dziś, dziękuję za uwagę.

Łukasz