MobX observer does not react on observable change - mobx

Here is my observable:
class AppState implements IAppState {
private static instance: AppState;
public static getInstance(): AppState {
if (!AppState.instance) {
AppState.instance = new AppState(AppState.initState);
}
return AppState.instance;
}
static initState: IState = {
isLoading: true,
isSignedIn: false,
};
private appState: IState;
private constructor(state: IState = AppState.initState) {
makeAutoObservable(this);
this.appState = state;
}
set isLoading(isLoading: boolean) {
runInAction(() => (this.appState.isLoading = isLoading));
}
set isSignedIn(isSignedIn: boolean) {
runInAction(() => (this.appState.isSignedIn = isSignedIn));
}
get state() {
return this.appState;
}
}
And here I observe it:
const StackNavigator = observer(() => {
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
gestureEnabled: false,
}}
>
{!AppState.state.isSignedIn ? (
<>
<Stack.Screen name="Login" component={WelcomeScreen} />
<Stack.Screen name="Signup" component={SignUpVM} />
</>
) : (
<>
<Stack.Screen name="Main" component={MainScreen} />
</>
)}
</Stack.Navigator>
);
});
But when AppState.state.isSignedIn changes the re-render doesn't happen. What could be the reason? May it be connected with the fact I use observable on React Navigation component, not custom one? Any help would be appreciated.

Try changing the order in the constructor:
private constructor(state: IState = AppState.initState) {
this.appState = state;
makeAutoObservable(this);
}
From the docs
make(Auto)Observable only supports properties that are already defined. Make sure your compiler configuration is correct, or as work-around, that a value is assigned to all properties before using make(Auto)Observable. Without correct configuration, fields that are declared but not initialized (like in class X { y; }) will not be picked up correctly.

Related

React Native Linking Deep Link Doesn't Open on First Load

I'm trying to enable deeplink in my React Native project, and have it working for all use-cases where the deeplink opens the app from the background, not from the first load. I believe the issue is that my path that I am deep-linking, is not available until after the "loading screen" has gone away.
For example I have this setup as my RootStack.Navigator
<RootStack.Navigator
detachInactiveScreens={false}
headerMode="none"
initialRouteName={"AppTabsScreen"}
screenOptions={{ animationEnabled: false }}
mode="modal"
>
{isLoading ? (
<RootStack.Screen
name="LoadingScreen"
component={LoadingScreen}
options={{ animationEnabled: true }}
/>
) : user ? (
<RootStack.Screen name="AppTabsScreen" component={AppTabsScreen} />
) : (
<RootStack.Screen name="AuthStackScreen" component={AuthStackScreen} />
)}
...
And my linking config as:
const deepLinksConf = {
screens: {
AppTabsScreen: {
screens: {
initialRouteName: "Activity",
Activity: {
screens: {
Activity: "activity",
Details: "workout/:userId/:id",
},
},
Goals: {
screens: {
Goals: "goal",
GoalDetail: "goal/:id",
},
},
Settings: "settings",
Profile: "profile",
},
},
}
};
In my getInitialURL() function, I get the URL correctly, but I believe the path is already on the LoadingScreen, and not the AppTabsScreen, which causes it not to fire. Is there anyway around this situation? How can I delay the deeplink to wait until the AppTabScreen is present to process the link? This works fine when the app is the fore or background, but never at first start.
....
async getInitialURL() {
console.log("fired");
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
console.log("deeplinking");
console.log(url);
if (url != null) {
console.log("just returning");
return url;
}
// Check if there is an initial firebase notification
const message = await messaging().getInitialNotification();
// Get deep link from data
// if this is undefined, the app will open the default/home page
return messsage.data?.link;
},
I am using react-navigation, and my main navigation controller is as follows:
<NavigationContainer
linking={linking}
theme={scheme === "dark" ? DarkTheme : DefaultTheme}
ref={navigationRef}
onReady={() => {
if (navigationRef.current) {
routeNameRef.current = navigationRef?.current.getCurrentRoute().name;
}
}}
onStateChange={async () => {
const previousRouteName = routeNameRef.current;
const currentRouteName = navigationRef?.current.getCurrentRoute().name;
if (previousRouteName !== currentRouteName) {
await analytics().logScreenView({
screen_name: currentRouteName,
screen_class: currentRouteName,
});
}
// Save the current route name for later comparision
routeNameRef.current = currentRouteName;
}}
>
<FlashMessage position="top" />
<RootStackScreen />
</NavigationContainer>

where I should initiate and connect socket.io in React Native component?

i now learning socket.io to build a chat app with React Native that contain private messaging like line/whatsapp, user can add another user to their friendlist, and can start chat with them...
Here's my Stack Navigation looks like:
class MainNavigator extends Component {
render() {
const { user } = this.props;
return (
<Stack.Navigator>
{user.username ? (
<>
<Stack.Screen name="ChatList" component={ChatList} options={{ title: 'Chatz' }} />
<Stack.Screen
name="Chat"
component={Chat}
options={{ title: `Chat as "${user.username}"` }}
/>
</>
) : (
<>
<Stack.Screen name="Login" component={Login} options={{ headerShown: false }} />
<Stack.Screen name="Register" component={Register} options={{ headerShown: false }} />
</>
)}
</Stack.Navigator >
);
}
}
You can see that if there is a user logged in in my app, stack navigator only contain ChatList and Chat, where the ChatList is a list of all active Chats.
I know that to connect a socket to backend we simply connect it with socket = io(http://localhost:3000), but where i should connect it in my components?
I'll use the socket in ChatList and Chat component.
Should i connect it in both of the component constructor like this?
class ChatList extends Component {
constuctor(props) {
super()
this.socket = io('localhost:3000');
}
.......
}
class Chat extends Component {
constuctor(props) {
super()
this.socket = io('localhost:3000');
}
.......
}
Or somewhere else just once? Because if i did it like above, it connecting several times to my express backend...
You should only connect to socket server once and switch connection when needed. My suggestion is create a class service that control connection logic and all component that need to listen to this connection channel will subcribe to the service. Example:
class SocketService {
constructor() {
this.connection = null
this.listener = []
}
connect(server) {
this.connection = io(server)
this.connection.<on_receive_messages> = this.onMessageReceived(data)
}
addListener(identify, callback = () => {}) {
this.listener[identify] = callback
}
removeListener(identify) {
delete this.listener[identify]
}
onMessageReceived(data) {
this.listener.map(callback => callback(data))
} ​
}
Then once user logged in, let connect your service
onUserLoggedIn() {
socket = new SocketService();
socket.connect("localhost:3000");
navigation.navigate("ChatList");
}
And inside your component
class ChatList extends Component {
componentDidMount() {
socket.addListener("ChatList", (message) => {
Alert.alert("message", message)
})
}
componentWillUnmount() {
socket.removeListener("ChatList");
}
.......
}
class Chat extends Component {
componentDidMount() {
socket.<switch_channel_or_something>
socket.addListener("Chat", (message) => {
Alert.alert("message", message)
})
}
componentWillUnmount() {
socket.removeListener("Chat");
}
}
It's not a complete code but you might get the idea

Why is my code causing my component to show up for a split second?

I am developing an application for a religious group, and I just implemented AsyncStorage to store the JWT received from my back end. My back end is working perfectly, and I'm able to store and delete the token on login/register and logout. But when the app is refreshed, it shows the login screen for a split second.
Here is my action:
export const fetchUser = () => {
return async (dispatch) => {
const token = await getData();
const reqObj = {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
};
if (token) {
dispatch({ type: "IS_LOADING_JWT" });
fetch(`http://${IP_ADDRESS}:4000/api/v1/auto_login`, reqObj)
.then((resp) => resp.json())
.then((current_user) => {
dispatch({ type: "LOGIN_SUCCESS", user: current_user });
dispatch({ type: "NOT_LOADING_JWT" });
});
}
};
};
Here is my helper function to retrieve the stored value-key pair:
const getData = async () => {
try {
const token = await AsyncStorage.getItem("jwt");
return token;
} catch (error) {
console.log(error);
}
};
This is my user reducer:
const user = (state = null, action) => {
switch (action.type) {
case "LOGIN_SUCCESS":
return action.user;
case "LOGOUT_USER":
return null;
default:
return state;
}
};
export default user;
Here is my 'authentication flow' with my stack navigator:
class Navigator extends Component {
componentDidMount() {
this.props.fetchUser();
}
render() {
return (
<NavigationContainer>
{this.props.jwtLoader ? (
<Stack.Navigator
screenOptions={{
title: "",
headerShown: false,
animationEnabled: false,
}}
>
<Stack.Screen name="loader" component={LoaderScreen} />
</Stack.Navigator>
) : !this.props.user ? (
<Stack.Navigator
screenOptions={{
title: "",
headerShown: false,
animationEnabled: false,
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
</Stack.Navigator>
) : (
<Stack.Navigator
screenOptions={{
title: "",
headerShown: false,
animationEnabled: false,
}}
>
<Stack.Screen name="Main" component={MainScreen} />
</Stack.Navigator>
)}
</NavigationContainer>
);
}
}
My jwtLoader:
const jwtLoader = (state = false, action) => {
switch (action.type) {
case "IS_LOADING_JWT":
return true;
case "NOT_LOADING_JWT":
return false;
default:
return state;
}
};
export default jwtLoader;
My theory is that there is a delay on dispatching the action. That is why I believe my Login component shows for a split second.
I have already tried getting and setting my user during my SplashScreen load, but that doesn't work either.
Here is a demo video: Text
Can you try fetching user from the constructor instead of componentDidMount?

How to set navigation params from outside navigation stack?

I have a header in my app that needs to render a different button depending on whether or not the user has notifications. This is how I currently have it set up in my pages:
static navigationOptions = ({ navigation }) => {
return {
title: 'My Profile',
headerRight: () => (
<Button
type="clear"
icon={() => <Icon
type="material-community"
size={25}
name={UserProvider.bNotifications ? 'bell' : 'bell-outline'}
color={UserProvider.bNotifications ? COLORS.WARNING : COLORS.WHITE}
/>}
onPress={() => navigation.navigate('Notifications', null)}
/>
)
}
};
The problem is, if UserProvider.bNotifications changes value, the header button doesn't update unless the page is changed / rerendered.
I want to switch to use the navigation property that is passed into those navigationOptions, but I don't no how to access it from outside the navigation stack. UserProvider is a static class (NOT a component) so I can't access the navigation prop through the usual manner (or by using the useNavigation hook).
I do have a NavigationProvider class that has access to the NavigationContainer for the app. I use it to trigger navigation without components. Is there some way I can set the params on the navigation property using that same reference?
NavigationProvider:
import { NavigationActions } from 'react-navigation';
let _navigator;
function getNavigator() {
return _navigator;
}
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
function goBack() {
_navigator.dispatch(
NavigationActions.back()
);
}
export default {
navigate,
setTopLevelNavigator,
getNavigator,
goBack
};
The ref is set like this in my top level App component:
<AppContainer
ref={navigatorRef => {
console.log(navigatorRef.props.navigation);
NavigationProvider.setTopLevelNavigator(navigatorRef);
}}
/>
EDIT - UserProvider
This is just the gist of my UserProvider class, but should convey how it works.
export default class UserProvider {
private static _bNotifications: boolean;
static get bNotifications(): boolean {
if (!this.hasInitNotifications)
this.initNotficationWatch();
return this._bNotifications;
}
static set bNotifications(bNotifications: boolean) {
this._bNotifications = bNotifications;
}
static initNotficationWatch() {
//Firebase listener on notification
if(notifications){
this.bNotifications = true;
} else {
this.bNotifications = false;
}
}
}
How to set navigation params from outside navigation stack?
to do this you have to utilize the getParam method that comes from the navigation prop. the way I would di it would be to set a variable to a parameter that would equal to UserProvider.bNotifications.
static navigationOptions = ({ navigation }) => {
let notifications = navigation.getParam('notifications')
return {
title: 'My Profile',
headerRight: () => (
<Button
type="clear"
icon={() => <Icon
type="material-community"
size={25}
name={notifications ? 'bell' : 'bell-outline'}
color={notifications ? COLORS.WARNING : COLORS.WHITE}
/>}
onPress={() => navigation.navigate('Notifications', null)}
/>
)
}
};
you can set the param to an initial value by adding it as a second argument if needed navigation.getParam('paramName', 'param initial value')
To update the parameter you need to use the setParams method. For this you can use the useNavigation hook.
You can also do this inside of a functions instead of a element
// remember to const navigation = useNavigation()
<Button
title="Update param"
onPress={() => navigation.setParams({notifications: 'new value'})}
/>
you can also initiate the value this way... but I would recommend to initialize the value inside your navigationOptions
I haven't tested the code but it should work
RN DOCS: https://reactnavigation.org/docs/2.x/headers/

How to pass current state of App to Tab Navigation Screen

If I'm using React Navigation v5, what is the best way to pass the current state of a parent component (in my case, the main App) down through a Tab and Stack navigator to a screen that I'd like to use the current state in?
Following the documentation, I have created a stack navigator for each tab that holds the respective screens.
App.js contains a state that needs to be used for a few things. Most importantly, it will provide badge count on the Tab navigator, as well as be a source of Flatlist data on one of the tab screens.
What is the correct approach to getting the state from App all the way down to a child component in a stack navigator in a tab navigator?
App.js
const Tab = createBottomTabNavigator()
export default class App extends React.Component {
constructor(props){
super(props)
this.state = {
neededArray: []
}
}
const updateTheArray = (newArray) => {
this.setState({
neededArray: newArray
})
}
componentDidMount(){
//Listener that searches for nearby bluetooth beacons and updates the array with the passed function
startObserver(updateTheArray)
}
componentWillUnmount(){
stopObserver()
}
render(){
return(
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen
name = "Home"
component = { HomeStack }/>
<Tab.Screen
name = "About"
component = { AboutStack }/>
//The Stack that contains the screen that I need to use the App's state in
<Tab.Screen
name = "Nearby"
component = { NearbyStack }/>
</Tab.Navigator>
</NavigationContainer>
)
}
}
NearbyStack.js
//This stack holds the screen that I need to use the App's state in
const NearbyStackNav = createStackNav()
const NearbyStack = () => {
return(
<NearbyStackNav.Navigator>
<NearbyStackNav.Screen
name = "Nearby"
component = { NearbyScreen }
/>
</NearbyStackNav.Navigator>
)
}
NearbyScreen.js
//The screen that I want to use the App's state in
const NearbyScreen = () => {
return(
<View>
<FlatList
//Where I would like to use the App's state
/>
</View>
)
}
You can pass some initial params to a screen. If you didn't specify any params when navigating to this screen, the initial params will be used. They are also shallow merged with any params that you pass. Initial params can be specified with an initialParams prop:
Usage
<Tab.Screen
name = "Nearby"
component = { NearbyStack }
initialParams={{ arrayItem: this.state.neededArray }}
/>
NearbyScreen.js
React.useEffect(() => {
if (route.params?.arrayItem) {
// Post updated, do something with `route.params.arrayItem`
// For example, send the arrayItem to the server
}
}, [route.params?.arrayItem]);
My solution was to use React's Context API.
BeaconContext.js - New
import React from 'react'
const BeaconContext = React.createContext()
export default BeaconContext
App.js - Modified
import BeaconContext from './path/to/BeaconContext'
const Tab = createBottomTabNavigator()
export default class App extends React.Component {
constructor(props){
super(props)
this.state = {
neededArray: []
}
}
const updateTheArray = (newArray) => {
this.setState({
neededArray: newArray
})
}
componentDidMount(){
startObserver(updateTheArray)
}
componentWillUnmount(){
stopObserver()
}
render(){
return(
// Wrap the nav container in the newly created context!!!
<BeaconContext.Provider value = { this.state.neededArray }
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen
name = "Home"
component = { HomeStack }/>
<Tab.Screen
name = "About"
component = { AboutStack }/>
<Tab.Screen
name = "Nearby"
component = { NearbyStack }/>
</Tab.Navigator>
</NavigationContainer>
</BeaconContext.Provider>
)
}
}
NearbyStack.js - Unchanged
const NearbyStackNav = createStackNav()
const NearbyStack = () => {
return(
<NearbyStackNav.Navigator>
<NearbyStackNav.Screen
name = "Nearby"
component = { NearbyScreen }
/>
</NearbyStackNav.Navigator>
)
}
NearbyScreen.js - Modified
import BeaconContext from './path/to/BeaconContext'
const NearbyScreen = () => {
return(
<View>
//Wrap the component in the new context's consumer!!!
<BeaconContext.Consumer>
{
context => <Text>{ context }</Text>
}
</BeaconContext.Consumer>
</View>
)
}
I've been struggling with the exact same issue - when using the initialProps property to pass a state to a Tab.Screen the screen never receives any updates. It reads the intial state value once then nothing.
To make it work I skipped using the initialProps property and instead used the children property on Tab.Screen like so:
App containing <Tab.Navigator> and <Tab.Screen>:
const[myBool, setMyBool] = useState(false)
<Tab.Screen
name="MyTab"
children={() => (
<MySecondScreen passedStateParam={ myBool } />
)}
.
.
.
</Tab.Screen>
MySecondScreen consuming updates on passed myBool state:
export function MySecondScreen ({ passedStateParam }) {
const myPassedBoolState = passedStateParam
React.useEffect(() => {
if(myPassedBoolState) {
//Act upon App.tsx updating the state
}
}, [myPassedBoolState])
}
Not sure if I'm missing something when trying to perform this with the initialParams property but this way (using children property) I got it to work at least.