Things I Learned: React Native w/ Typescript Best Practices & Tricks.

josh sudung
5 min readMay 12, 2020

--

My first time developing a mobile app with React Native was also my first time using typescript. I’ve had small experiences developing with React, but some things needed to be re-explored in regard to patterns and best practices. Luckily, I’ve had it easy, as one of my group mates had already developed a mobile app with React Native typescript before. It’s just a matter of spamming his chat (To be fair, some questions I had aren’t Google-able, and he’s cool with it I guess) and learning from some of his earlier implementations. So here are some of the new things I learned when developing my PPL project with React Native typescript.

Source

‘Type’ everything.

In typescript, it is encouraged to have variables and function parameters to have a type. I believe the term for this is data binding, where we use concepts like types and interfaces as a way to describe data being used.

const initialValue: number = 0;const [filterType, setFilterType] = useState<string>('name');// Array of log objects
const [logList, setLogList] = useState<object[]>([]);
// Optional parameters are declared with a '?'.
// We can set a default value on an optional parameter like so
const fetchInvestigationCases = async (
isSearch: boolean
pageNumber?: number
query?: string
isFilter?: boolean = false
) => {...}

Take a look at the array of log objects. A better way to do it would be to use interfaces to declare an object type, by defining that logList only accepting objects with Log attributes, and no other.

interface Log {
id: string;
model_name: string;
revision_id: string;
object_id: string;
latest_version: string;
action_type: string;
recorded_at: string;
author: Author;
case_object: InvestigationCase;
latest: boolean;
}
const [logList, setLogList] = useState<Log[]>([]);// Now I can destructure the objects without any typescript errors
for (let log in logList) {
const {id, case_object, author} = log;
...
}

Interfaces are also used to declare a function parameter types or a functional component prop type.

interface HeaderProps {
headerText: string;
isHome?: boolean;
greetingName?: string;
onClick?: () => void;
}
const Header = ({
headerText,
isHome = false,
greetingName = '',
onClick = () => {},
}: HeaderProps) => { ... }

Optional Chaining

I find this one particularly useful when doing endpoint requests. In development, we did a lot of testing by creating data with null attribute values, when in fact later in development, those attributes are required and not optional. Normally, I’d have to do a lot of null/undefined checking to avoid the error we all hate: undefined is not an object. Optional chaining lets me get away with this (We did implement proper error handling later on, of course).

Let’s take a look at an example. Our Investigation Case objects should have a case_subject object as its attribute.

interface InvestigationCase {
is_edit: boolean;
id: string;
case_subject: CaseSubject;
reference_case: string;
reference_case_id: string;
case_relation: string;
medical_symptoms: string;
risk_factors: string;
is_referral_needed: boolean;
medical_facility_district: string;
medical_facility_reference: string;
author: string;
}
interface CaseSubject {
id: string;
name: string;
age: number;
is_male: string;
address: string;
district: string;
sub_district: string;
}

Some of our Investigation Case data does not have a case subject, hence accessing its attributes will yield undefined is not an object. So what I did was use optional chaining (adding ?), so whenever case_subject is null/undefined, it doesn’t throw any errors and instead returns undefined.

// Before
global.setCache({
investigationCaseFormData: {
...invesCase,
case_subject = {
...invesCase.case_subject,
// Both these lines below will throw an error.
age: invesCase.case_subject.age.toString(),
is_male: invesCase.case_subject.is_male ? 'Pria' : 'Wanita',
}
},
});
// After
global.setCache({
investigationCaseFormData: {
...invesCase,
case_subject = {
...invesCase.case_subject,
// Now these lines will automatically return undefined
// if invesCase.case_subject is undefined or null
age: invesCase.case_subject?.age.toString(),
is_male: invesCase.case_subject?.is_male ? 'Pria' : 'Wanita',
}
},
});

You might think this isn’t a big deal, but it is indeed useful as a ‘safeguard’ when dealing with unexpectedundefined ornull.

Utilize Promise functions on async events

This one took me a while to figure out, and while it isn’t typescript or react native specific, it is still useful to learn when dealing with asynchronous events.

I have an array of logs. In my use case, I needed to fetch each log’s case object before rendering the page. What I initially did was this.

// This is declared on the beginning of the component
const [logList, setLogList] = useState<Log[]>([]);
// This is a snippet from the useEffect callback before renderlet tempLogList: Log = logList;
tempLogList.map((log: Log) => {
const response = await main.getInvestigationCase(log.object_id);
log.case_object = response.data;
})
setLogList(tempLogList);

This should work, right? I mean, we have an await for the async function (an axios request that returns a Promise). Surely by the time the component renders, we have an updated logList with each of the logs having a case object? Up until now, I still don’t have the slightest idea of why this doesn’t work. Then again, if it wasn’t for my friend, I wouldn’t be able to solve this.

let tempLogList: Log = logList;Promise.all(
tempLogList.map(async (log: Log) => {
const response = await main.getInvestigationCase(log.object_id);
log.case_object = response.data;
return response;
})
).then((_res) => setLogList(tempLogList));

Sometimes we do forget that the foundation of async/await events in javascript are promises.

useContext

useContext is one of the three basic hooks implemented in React 16.8, besides useState and useEffect. A context helps us to handle state/props without passing it down on each component. There may be an alternative to achieve this with Redux. I personally haven’t tried Redux yet, so here’s my biased take on this topic. Contexts help us declare a global state, which can easily be accessed in all of the child components. Creating our context :

import { createContext } from 'react';const AppContext = createContext<any>({});export default AppContext;

Assigning variables, state, and global values to the context via the providerAppContext.Provider in the parent component:

const contextValue: GlobalProps = {
// Fetch services
services: {
main: mainService,
},
user,
setUser,
setToken,
shouldLoading,
setShouldLoading,
setAlert,
isAuthenticated: !!token,
// Device screen size
vw: Dimensions.get('window').width,
vh: Dimensions.get('window').height,
// Form caching
cache,
setCache: (data) => {
cacheDispatch({
type: CacheActionType.Update,
data,
})
},
};
return (
<AppContext.Provider value={contextValue}>
... // Here we set up a router or a navigator that serves all components that needs the context
</AppContext.Provider>

Remember that contexts aren’t really mutable. So don’t forget to include their mutators if you wish to modify their values. Now we have set up the context, we can consume (use) it inside our child components with useContext:

import { useContext } from 'react';const ContactInvestigationFormStep1 = () => {
const { services, setCache, user } = useContext(AppContext);
... // Do things with these global states.
return (
<>
...
</>
}

Written as an assignment for my software development project individual review.

--

--

josh sudung
josh sudung

No responses yet