How to have a custom font hook acessible from pages - react-native

I have got back into React Native after a few years and wanted to get a custom font. I tried looking through the documentation but they only have a way how to do it on the page itself and you can't use styles. They mentioned
Font.LoadAsync()
But that too is not working for me as I am using a function rather than a class.
I looked into the code I did a few years back and found out that the way I did this was this:
import * as Font from "expo-font";
export default useFonts = async () =>
await Font.loadAsync({
'CantoraOne': require('../assets/fonts/CantoraOne-Regular.ttf'),
});
export default function Dice() {
const [IsReady, SetIsReady] = useState(false);
const LoadFonts = async () => {
await useFonts();
};
if (!IsReady) {
return (
<AppLoading
startAsync={LoadFonts}
onFinish={() => SetIsReady(true)}
onError={() => { }}
/>
);
}
return (
<Text style={styles.text}>Some text</Text>
)
}
const styles = StyleSheet.create({
text: {
color: '#fff',
fontFamily: 'CantoraOne',
fontSize: 50,
},
})
This worked but when I tried doing the same workaround now. I found out that "App Loading" is deprecated and should not be used. I don't know of any other way how to do it and I feel lost.
How can I make a central place from which I can import a custom font into every page on my project without needing to write "Font.LoadAsync()" into every page?

AppLoading is deprecated because the functionality is easily created on your own. Check out the usage recommended in the current docs:
https://docs.expo.dev/versions/latest/sdk/font/#usage
Keep loading state at the root level to track whether the fonts are loaded, starting with true
Load the fonts in a useEffect, set loading to false when done
Return a loading indicator while the fonts are unloaded and the app when they are done

Related

Expo React Native download a pdf file from server and save it in the local file system

I am having a hard time understanding the process of downloading a file pdf or excel from let's say a nodejs server in ReactNative using Expo.
So far I have gone through a bunch of blog posts/resources on the web but unfortunately I didn't find any solution for this. Most of the blogs are outdated or they don't have any clear implementation for it.
Here are a few approach that I came across.
using rn-fetch-blob but this does not work with expo as per my findings and each time I tried to integrate it with Expo, I ended up with error.
The second approach to use expo's FileSharing approach, which kind of works but only if you have a predefined image or pdf file.
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react";
import { StyleSheet, Text, View, Button, Alert } from "react-native";
import * as FileSystem from "expo-file-system";
import * as Sharing from "expo-sharing";
export default function App() {
const [downloadProgress, setDownloadProgress] = useState(0);
const [document, setDocument] = useState(null);
async function openShareDialogAsync() {
if (!(await Sharing.isAvailableAsync())) {
alert(`Uh oh, sharing isn't available on your platform`);
return;
}
Sharing.shareAsync(document);
}
async function handleDownload() {
const callback = (downloadProgress) => {
const progress =
downloadProgress.totalBytesWritten /
downloadProgress.totalBytesExpectedToWrite;
setDownloadProgress(progress * 100);
};
let url =
"https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf";
const downloadResumable = FileSystem.createDownloadResumable(
url,
FileSystem.documentDirectory + "a.pdf",
{},
callback
);
try {
const { uri } = await downloadResumable.downloadAsync();
console.log("Finished downloading to ", uri);
setDocument(uri);
} catch (e) {
console.error(e);
}
}
useEffect(() => {
handleDownload();
}, []);
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<Text>Welcome Saroj!</Text>
<Button title="Share" onPress={openShareDialogAsync} />
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
This implementation kind of works. I am able to share the pdf but the issue is the pdf link is not generated on the fly, it is something already being generated previosuly. I expect something like we can call an endpoint to a server and the server would either send base64 or binary data that we can use to make the pdf download to the local storage of the device.
I am open to suggestions and would really appreciate your inputs.
Use Blob and FileReader to download any binary data. Something like this:
const response = await fetch('<link>');
const data = await response.blob()
const reader = new FileReader();
reader.onload = async () => {
const fileUri = FileSystem.documentDirectory + "file";
await FileSystem.writeAsStringAsync(
fileUri, reader.result.split(',')[1],
{ encoding: FileSystem.EncodingType.Base64 }
);
};
reader.readAsDataURL(data);

Warning: React has detected a change in the order of Hooks

I have run into this error in my code, and don't really know how to solve it, can anyone help me?
I get the following error message:
ERROR Warning: React has detected a change in the order of Hooks called by ScreenA. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
import React, { useCallback, useEffect, useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useNavigation } from '#react-navigation/native';
import { DancingScript_400Regular } from "#expo-google-fonts/dancing-script";
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Keep the splash screen visible while we fetch resources
await SplashScreen.preventAutoHideAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync({ DancingScript_400Regular });
// Artificially delay for two seconds to simulate a slow loading
// experience. Please remove this if you copy and paste the code!
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
// This tells the splash screen to hide immediately! If we call this after
// `setAppIsReady`, then we may see a blank screen while the app is
// loading its initial state and rendering its first pixels. So instead,
// we hide the splash screen once we know the root view has already
// performed layout.
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
const navigation = useNavigation();
const onPressHandler = () => {
// navigation.navigate('Screen_B', { itemName: 'Item from Screen A', itemID: 12 });
}
return (
<View style={styles.body} onLayout={onLayoutRootView}>
<Text style={styles.text}>
Screen A
</Text>
<Pressable
onPress={onPressHandler}
style={({ pressed }) => ({ backgroundColor: pressed ? '#ddd' : '#0f0' })}
>
<Text style={styles.text}>
Go To Screen B
</Text>
</Pressable>
<Text style={styles.text}>{route.params?.Message}</Text>
</View>
)
}
const styles = StyleSheet.create({
body: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 40,
margin: 10,
fontFamily: 'DancingScript_400Regular'
}
})
I have read the rules of hooks: https://reactjs.org/docs/hooks-rules.html
The output is correct, but i want to fix this error before i add more additions to the app
You need to move useNavigation use before early returns.
Instead, always use Hooks at the top level of your React function, before any early returns.
The key is you need to call all the hooks in the exact same order on every component lifecycle update, which means you can't use hooks with conditional operators or loop statements such as:
if (customValue) useHook();
// or
for (let i = 0; i< customValue; i++) useHook();
// or
if (customValue) return;
useHook();
So moving const navigation = useNavigation(); before if (!appIsReady) {return null;}, should solve your problem:
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
const navigation = useNavigation();
// ...
}

How to restart app (react native and expo)

I use expo so I've no access to android folder.
I want to restart my app for first time. How can I do that?
I use react-native-restart, but not wroking and I have an error now:
null is not an object (evaluating 'x.default.restart;)
Codes:
componentDidMount() {
if (I18nManager.isRTL) {
I18nManager.forceRTL(false);
RNRestart.Restart();
}
}
How Can I restart my app?
I've had the same problem for over a month, nothing helped me, so I developed a library to accomplish this, simple install it using:
npm i fiction-expo-restart
and import it like:
import {Restart} from 'fiction-expo-restart';
and then when you want to perform a restart, use:
Restart();
Note in case this answer gets old, you can check the library here: https://www.npmjs.com/package/fiction-expo-restart
I have faced the same issue and found this solution somewhere.
You can try to use Updates from expo like this:
import { Updates } from 'expo';
Updates.reload();
import { StatusBar } from "expo-status-bar";
import React from "react";
import { Button, I18nManager, StyleSheet, Text, View } from "react-native";
import * as Updates from "expo-updates";
async function toggleRTL() {
await I18nManager.forceRTL(I18nManager.isRTL ? false : true);
await Updates.reloadAsync();
}
export default function App() {
return (
<View style={styles.container}>
<Text>{new Date().toString()}</Text>
<Text>{I18nManager.isRTL ? "RTL" : "LTR"}</Text>
<View style={{ marginVertical: 5 }} />
<Button title="Reload app" onPress={() => Updates.reloadAsync()} />
<View style={{ marginVertical: 5 }} />
<Button title="Toggle RTL" onPress={() => toggleRTL()} />
<StatusBar style="auto" />
</View>
);
}
https://github.com/brentvatne/updates-reload/blob/master/App.js
It's the only working way for me. When i try automatically reload app in useEffect - it crashes, so i make a separate screen where i ask user to press button to reload app
For Expo SDK 45+ please use
import * as Updates from "expo-updates"
Updates.reloadAsync()
The module fiction-expo-restart is not maintained anymore.
If you are using react-native-code-push library, you can restart with this;
import CodePush from 'react-native-code-push';
CodePush.restartApp();
What I did was to build a Restart component that is not a const but a var. And an applyReload() function that sets that var to an empty component <></> if the reload bool state is true, triggering the re-render.
The re-render will reinstate the Restart var back to its original structure, but a new instance is then created, effectively reloading everything that is inside the <Restart> tag:
My App.tsx:
export default function App() {
const [reload, setReload] = useState(false);
type Props = { children: ReactNode };
var Restart = ({ children }: Props) => {
return <>{children}</>;
};
const applyReload = () => {
if (reload) {
Restart = ({ children }: Props) => {
return <></>;
};
setReload(false);
}
};
useEffect(applyReload);
useEffect(() => {
// put some code here to modify your app..
// test reload after 6 seconds
setTimeout(() => {
setReload(true);
}, 6000);
}, []);
return (
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }}>
<PaperProvider theme={appTheme}>
<NavigationContainer theme={appTheme} documentTitle={{ enabled: false }}>
<AppContext.Provider value={appContext}>
<Restart>
<MyMainAppComponent />
</Restart>
</AppContext.Provider>
</NavigationContainer>
</PaperProvider>
</SafeAreaView>
</SafeAreaProvider>
);
I also added the 'setReload' state function to my '<AppContext.Provider>' so anywhere down my App it is possible to trigger the App reload.

Is there a way to set a font globally in React Native?

I need to create a custom font that applies to every Text component in the whole application.
Is there is a way to set a font globally in React Native?
One way is to create a wrapper for RN Text say MyTextCustomFont:
const MyTextCustomFont = (props) => {
return (
<Text style={{fontFamily:'myFont'}} {...props} >{props.children}</Text>
)
}
import this MyTextCustomFont and use anywhere.
Another way is to define a style object and use it wherever you want.
To do this we have to implement a method in which we will override Text component creation in React Native. In this we will set default font family or size or any attribute we want to set by default.
// typography.js
import React from 'react'
import { Text, Platform, StyleSheet } from 'react-native'
export const typography = () => {
const oldTextRender = Text.render
Text.render = function(...args) {
const origin = oldTextRender.call(this, ...args)
return React.cloneElement(origin, {
style: [styles.defaultText, origin.props.style],
})
}
}
const styles = StyleSheet.create({
defaultText: {
fontFamily: 'NunitoSans-Regular',//Default font family
}
});
Then in index.js you have to do this:
import { typography } from './src/utils/typography'
typography()
Detailed answer here:
https://ospfolio.com/two-way-to-change-default-font-family-in-react-native/
I think your problem is add Custom Fonts in react native.
1. Add Your Custom Fonts to Assets
Add all the font files you want to an “assets/fonts” folder in the root of your react native project:
2. Edit Package.json
Adding rnpm to package.json providing the path to the font files:
"rnpm": {
"assets": [
"./assets/fonts/"
]
},
3. Link assest files
run this command in your react native project root folder
react-native link
This should add the font references in your Info.plist file for iOS and on Android copy the font files to android/app/src/main/assets/fonts.
4. Add in stylesheet
Add a fontFamily property with your font name:
const styles = StyleSheet.create({
title: {
fontSize: 16,
fontFamily: 'PlayfairDisplay-Bold',
color: '#fff',
paddingRight: 20,
},
});
So, I've made a component doing this quite easely some times ago. This is working with Expo, I don't know for vanilla react-native.
at the start of your app:
import { Font, Asset } from 'expo'
async initFont() {
try {
await Font.loadAsync({
'Bariol': require('src/assets/font/Bariol_Regular.otf'),
'Bariol Bold': require('src/assets/font/Bariol_Bold.otf'),
})
this.setState({ fontReady: true })
} catch (e) {
console.log(e)
}
}
Then, you have to create a component file like text.js containing this code:
export default function (props) {
let font = { fontFamily: 'Bariol' }
if (props.bold) {
font = { fontFamily: 'Bariol Bold' }
}
const { bold, style, children, ...newProps } = props
return (
<Text {...newProps} style={[Style.text, props.style, font]}>
{props.children}
</Text>
)
}
Finally, in any of you other component / page just import MyText:
import Text from 'path/to/text.js'
use it like a normal Text component:
<Text bold>Hello World!</Text>
Even if this solution looks a bit more complicated than the others, it is easier to use once the setup is ok, since you just have to import Text.
You can override Text behaviour by adding this in any of your component using Text:
Edit: Add this code in your App.js or main file
let oldRender = Text.render;
Text.render = function (...args) {
let origin = oldRender.call(this, ...args);
return React.cloneElement(origin, {
style: [{color: 'red', fontFamily: 'Arial'}, origin.props.style]
});
}
For react Native Version 0.56 or below, Add this code in your App.js or main file
let oldRender = Text.prototype.render;
Text.prototype.render = function (...args) {
let origin = oldRender.call(this, ...args);
return React.cloneElement(origin, {
style: [{color: 'red', fontFamily: 'Arial'}, origin.props.style]
});
};
Reference
Or create your own component, such as MyAppText.
MyAppText would be a simple component that renders a Text component using your universal style and can pass through other props, etc.
I use a wrapper with default props like this :
const CustomText = ({ fontFam = "regular", ...props }) => {
const typo = {
light: "Montserrat_300Light",
regular: "Montserrat_400Regular",
bold: "Montserrat_600SemiBold",
};
return (
<Text {...props} style={[{ fontFamily: typo[fontFam], ...props.style }]}>
{props.children}
</Text>
);
};
export default CustomText;
By default, if "fontFam" is not indicated it will be regular font.
An example with bold typo :
<CustomText fontFam="bold" style={{ marginTop: 30, color: "grey" }}>
Some Text
</CustomText>
You can replace all your <Text/> by <CustomText />.
If you don't have to create custom component, you could try react-native-global-font. It will be apply for your all Text and TextInput
yes
app.js
import styles from './styles';
{...}
<Text style={styles.text}>hello World </Text>
{...}
styles.js
import {StyleSheet} from 'react-native';
const styles = StyleSheet.create({
text: {
// define your font or size or whatever you want to style here
},
use style on every text and all changes will affect all text components

Expo.FileSystem.downloadAsync do not show download notification

I am using expo FileSystem to download the pdf file. The API response lands into success function. However, I am not able to show the downloaded file to the user.
The expected behaviour should be like we usually see notification icon on the status bar and on click on icon its opens your file.
FileSystem.downloadAsync(
'https://bitcoin.org/bitcoin.pdf',
FileSystem.documentDirectory + 'Stay_Overview.xlsx'
).then(({ uri }) => {
console.log('Finished downloading to ', uri);
})
.catch(error => {
console.error(error);
});
This one had one or two tricks, but here is a solution to this using Expo that works on both iOS and Android.
In a new Expo project, amend the following two files:
App.js
import React, { Component } from 'react';
import { View, ScrollView, StyleSheet, Button, Alert, Platform, Text, TouchableWithoutFeedback } from 'react-native';
import { FileSystem, Constants, Notifications, Permissions } from 'expo';
import Toast, {DURATION} from 'react-native-easy-toast';
async function getiOSNotificationPermission() {
const { status } = await Permissions.getAsync(
Permissions.NOTIFICATIONS
);
if (status !== 'granted') {
await Permissions.askAsync(Permissions.NOTIFICATIONS);
}
}
export default class App extends Component {
constructor(props) {
super(props);
// this.toast = null;
this.listenForNotifications = this.listenForNotifications.bind(this);
// this.openFile = this.openFile.bind(this);
this.state = {
filePreviewText: ''
}
}
_handleButtonPress = () => {
let fileName = 'document.txt';
let fileUri = FileSystem.documentDirectory + fileName;
FileSystem.downloadAsync(
"https://raw.githubusercontent.com/expo/expo/master/README.md",
fileUri
).then(({ uri }) => {
console.log('Finished downloading to ', uri);
const localnotification = {
title: 'Download has finished',
body: fileName + " has been downloaded. Tap to open file.",
android: {
sound: true,
},
ios: {
sound: true,
},
data: {
fileUri: uri
},
};
localnotification.data.title = localnotification.title;
localnotification.data.body = localnotification.body;
let sendAfterFiveSeconds = Date.now();
sendAfterFiveSeconds += 3000;
const schedulingOptions = { time: sendAfterFiveSeconds };
Notifications.scheduleLocalNotificationAsync(
localnotification,
schedulingOptions
);
})
.catch(error => {
console.error(error);
Alert.alert(error);
});
};
listenForNotifications = () => {
const _this = this;
Notifications.addListener(notification => {
if (notification.origin === 'received') {
// We could also make our own design for the toast
// _this.refs.toast.show(<View><Text>hello world!</Text></View>);
const toastDOM =
<TouchableWithoutFeedback
onPress={() => {this.openFile(notification.data.fileUri)}}
style={{padding: '10', backgroundColor: 'green'}}>
<Text style={styles.toastText}>{notification.data.body}</Text>
</TouchableWithoutFeedback>;
_this.toast.show(toastDOM, DURATION.FOREVER);
} else if (notification.origin === 'selected') {
this.openFile(notification.data.fileUri);
}
// Expo.Notifications.setBadgeNumberAsync(number);
// Notifications.setBadgeNumberAsync(10);
// Notifications.presentLocalNotificationAsync(notification);
// Alert.alert(notification.title, notification.body);
});
};
componentWillMount() {
getiOSNotificationPermission();
this.listenForNotifications();
}
componentDidMount() {
// let asset = Asset.fromModule(md);
// Toast.show('Hello World');
}
openFile = (fileUri) => {
this.toast.close(40);
console.log('Opening file ' + fileUri);
FileSystem.readAsStringAsync(fileUri)
.then((fileContents) => {
// Get file contents in binary and convert to text
// let fileTextContent = parseInt(fileContents, 2);
this.setState({filePreviewText: fileContents});
});
}
render() {
return (
<View style={styles.container}>
<View style={styles.buttonsContainer}>
<Button style={styles.button}
title={"Download text file"}
onPress={this._handleButtonPress}
/>
<Button style={styles.button}
title={"Clear File Preview"}
onPress={() => {this.setState({filePreviewText: ""})}}
/>
</View>
<ScrollView style={styles.filePreview}>
<Text>{this.state.filePreviewText}</Text>
</ScrollView>
<Toast ref={ (ref) => this.toast=ref }/>
</View>
);
// <Toast
// ref={ (ref) => this.toast=ref }
// style={{backgroundColor:'green'}}
// textStyle={{color:'white'}}
// position={'bottom'}
// positionValue={100}
// opacity={0.8}
// />
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
},
buttonsContainer: {
flexDirection: 'row',
},
button: {
flex: 1
},
filePreview: {
flex: 1,
padding: 10,
},
toastText: {
color: 'white',
padding: 5,
justifyContent: 'flex-start',
},
});
package.json: Add the following dependency (fork of react-native-easy-toast)
"react-native-easy-toast": "git+https://github.com/SiavasFiroozbakht/react-native-easy-toast.git"
There are a couple of important notes about this solution:
Uses Expo API the most, for external local notifications and writing to / reading from files, which limits the current solution to being unable to write to other locations than Expo's own directory.
Once the file is downloaded, either a customisable toast is shown to the user if the app is active (Expo currently does not support foreground notifications), or sends a local Push Notification to let the user know the download has finished. Clicking on any of these two will show the contents of the file in a View, using the <Text> component.
The crazycodeboy/react-native-easy-toast repo has not been used directly due to a limitation of the toast, which is that the touch events are currently disregarded. The forked repo makes this functionality available before the merge request is implemented in the original. I recommend switching back to the original one once it gets patched as I will likely not maintain mine.
Although this project is also available in Snack, it will not run due to the need of using a git repository in package.json as mentioned above, and other apparent inconsistencies in variable scoping. This would be fixed by either the merge request or the new feature in Snack.
Other file types may be supported, either by Expo itself or via external packages, such as this PDF viewer. However, the code will have to be further adapted.
The toast (internal notification) is created with a TouchableWithoutFeedback component, although there are other similar ones in React Native with various differences. This component can be customised in the code (search for toastDOM), but might even be replaceable in the future by internal notifications available in Expo.
Lastly, an intentional three-second delay is applied to the notification once the file is downloaded – this allows us to test the notification when the app is in background. Feel free to remove the delay and trigger the notification immediately.
And that's it! I think this gives a good starting point for file downloading and previewing with Expo.
Codebase also available on GitHub.
DownloadManager
I believe that you are looking to use the DownloadManager for handling your downloads on Android (be aware there is no DownloadManager for iOS so you would have to handle this differently) The DownloadManager either savse the file to a shared system cache or it would save it to external storage.
However, at this time I do not believe that Expo allows you to use the DownloadManager, instead handling all the downloads itself. The reason could be that Expo doesn't allow you access to external storage, as it states in the documentation:
Each app only has read and write access to locations under the following directories:
Expo.FileSystem.documentDirectory
Expo.FileSystem.cacheDirectory
https://docs.expo.io/versions/latest/sdk/filesystem
So there would be no way to access the file in Expo once it was downloaded.
Possible Solution
A possible solution would be to use React-Native Fetch Blob. It does allow you to use the DownloadManager.
https://github.com/joltup/rn-fetch-blob#android-media-scanner-and-download-manager-support
Using the DownloadManager can be achieved in rn-fetch-blob by setting the addAndroidDownloads with the useDownloadManager key set to true.
However, that would mean ejecting your application from Expo.