Change state from other component (without passing setState function) - react-native

I have a quite decent background in android but now I am starting digging into react native and I am really confused with the ways to change the state of a component through hooks and set state function.
To the point, I have my main screen and I have many small components which change visibility. This is done by letting the user change some filter settings within dialogs. So the suggested way to do that is by having a hook in my main screen with a list that holds the values for the visibility of each component. But since I change the visibility of the components from inside the modals, every time I want to show a modal I will have to pass in a different function(for example setComponentEnable or setComponentDisabled) to set the state for each component. So my main screen will be polluted from all these small functions. Also I should not forget to mention that my modals are consisted from many smaller components and I will have to pass as deep as it goes the proper function to match the user action.
So my question is, is there a way to do this thing without polluting my main with all these small functions and make it possible for the main screen to update every time the user change the filters within the modals?
I already read about context but the docs say:
Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.
So I dont think that this should be a great case for context use.
What I am trying to do now is create a hook with a list
const [isibility, setVisibility] = useState([]);
create visibility handler functions
const setVisibilityEnable = () => {
...
}
and pass it into my modal.
<MyModal
visibilityHandler={setVisibilityEnable}/>
Is there a way to manipulate the state without passing all these callbacks to the modals? Or maybe is there anyone that can suggest a better and clean solution to avoid end up having a really huge main screen?

you can include all the settings in one object and pass that object to all the components. Then each component will then modify that object accordingly.
const defaultVisibility = {
childComponentOne: true,
childComponentTwo: true,
};
const [visibilityObject, setVisibilityObject] = useState(defaultVisibility);
pass both the function and the object into your child components:
<ChildComponentOne visibilityObject={visibilityObject} setVisibilityObject={setVisibilityObject} />
Then in your child component, you set the visibility like so:
setVisibilityObject({...visibilityObject, childComponentOne: false});

Why you don't just pass a property to your modal and check if changed in oncomponentdidchange method?
componentDidUpdate(prevProps) {
if (this.props.yourPoperty!== prevProps.yourPoperty) {
//do your visibility stuff
}
}
Alternatively you can do it with redux when you connect your components to the store.

Related

How to reload a component or call a function after action in another component with React Native

I use React Native and Expo to develop an application on mobile and tablet.
The objective is to be able to answer a questionnaire where several questions are divided into various categories. A question may have a category or not, so they don't really follow each other in the code.
I use a loop to display my categories, with its questions inside. Questions are components, categories too.
When I answer a question, sometimes I want to reload another question specifically (or several). After answering my question, I have a list of questions I want to reload.
I would like either to launch a function in the component, or to reload it entirely.
How do I reload the question component, which may be quite far from the one I acted on?
Thanks for your help !
I wanted to listen with UseEffect for the modification of my global variable, but it doesn't work. Also, I can't fully reload my page either. I would like to specifically reload the question component or run the function.
What I have already tried:
Reloading the page entirely from my screen with a function in global, didn't work (
I have a lot of elements so I would like to avoid reloading everything)
Listen with useEffect the change of my list of questions to modify to check if my id was in it, but the listening is not done
I can't send functions or other elements in the props of my question because they can be in different components (category or other) so too far away.
Question Component (simplified) :
export default function RealisationQuestionComponent(props){
//When action in Question A
function changeCheckA(value) {
[...]
if (props.question.questions_impactees !== undefined) {
global.questions_impactees = props.question.questions_impactees;
}
}
//Effect in Question B
useEffect(() => {
[my function or reload entire component]
}, [global.questions_impactees]);
return (
<>...</>
)
EDIT :
Following the answer, I tried to use useContext to retrieve my information, but it tells me Can't find variable : RealContext
What i would like to do :
https://www.w3schools.com/react/showreact.asp?filename=demo2_react_context2
What I have done :
My screen AuditRealisationScreen
const RealContext = createContext();
const [listIdVictimes, setListIdVictimes] = useState('please test');
[...]
return (
<RealContext.Provider value={listIdVictimes}>
<RealisationChapitreComponent/>
</RealContext.Provider>
)
RealisationChapitreComponent > [...] > RealisationQuestionComponent
In my component RealisationQuestionComponent
const listTest = useContext(RealContext);
[...]
return (
<Text> {listTest} </Text>
)
Can it work this way?
Use [contexts][1]. Using [reducers][2] with context would be more flexible and organised.
If you have a lot of states to manage with mess of siblings and parent-child and child-parent relations then using [Redux][3] would give you more control to manage those components and its states
What I used do for
How to reload a component or call a function after action in another
component with React Native
const context = ... // declare
and then
// import context
const App=()=>{
...
return(
<Context.Provider value={your functions, data, states, props whatever you want}>
{{ ...child components }}
<Context.Provider/>
)
}
You can play with these contexts usage to achieve your goal
[1]: https://reactjs.org/docs/hooks-reference.html#usecontext
[2]: https://reactjs.org/docs/hooks-reference.html#usereducer
[3]: https://redux.js.org/introduction/why-rtk-is-redux-today#what-is-redux

track UI elements states with one object, but the states are not reserved once leaving the screen and coming back

In my react-native project, I have three checkboxes, I need to track the state of those checkboxes so I use an object with key-value (value is boolean) to represent the states of all three checkboxes and use useState hook to manage them. Here is my code:
import React, { useState, useEffect } from 'react';
...
const MyScreen = ({ navigation }) => {
// initially, all checkboxes are checked
const initialCheckBoxState = {
0: true,
1: true,
2: true,
};
const [checkBoxesState, setCheckBoxesState] = useState(initialCheckBoxState);
useEffect(() => {
return () => {
console.log('Screen did unmount');
};
}, [checkBoxesState]);
return (
<View>
...
<SectionList
sections={options}
renderItem={({ index, item }) => (
<CheckBox
onPress={() => {
const checkBoxesStateCopy = { ...checkBoxesState };
checkBoxesStateCopy[index] = !checkBoxesStateCopy[index];
setCheckBoxesState(checkBoxesStateCopy);
}}
/>
)}
/>
...
</View>
);
};
I omitted code that is not the concern of my problem. As you can see, for each item I draw one CheckBox component.
In practice, there are always three items (i.e. three check boxes to show). At the beginning I declared initialCheckBoxState, each key-pair represents the state of the checkbox of each. In the onPress callback of Checkbox I toggle each check box state & update the checkBoxesState by hook method setCheckBoxesState as a whole.
Everything works fine at runtime, my screen is re-rendered when toggling checkbox state, UI shows the status of checkboxes correctly. But issue comes when I navigate back to the previous screen and navigate back to this screen, all checkboxes states are back to the initial states.
So, why the checkboxes states are not reserved?
P.S. previous screen and MyScreen are under the same stack navigator. User press a button of previous screen to navigate to MyScreen. From MyScreen user can go to previous screen by pressing the "headerLeft" button
First lets answer the question:
why the checkboxes states are not reserved?
This component is handling its state completely independent, the state is created & handled inside and no values are passed-in from outside. what does it mean? this component has its initial state value inside of itself, it doesn't use any prop or anything else to initialize the state. everytime this component gets created, state is again initialized with that value. so that's the reason you lose all changes done to checkboxes, because when you leave this screen(component) , it gets unmounted(we'll talk about this in next question) and because all values are just handled inside, every data (containing checkboxes state) will be lost.
So now lets talk about this:
is react-native supposed to reserve the state when come back to the screen?
short answer is No. Every component is destroyed when unmounted including their state and data.
Now lets answer why
screens are still on the stack in memory, not destroyed?
Usually developers use a package like react-navigation or RNRF(which is built on top of react-navigation) for react navigation, most of times we don't care about how they handle this navigation logic, we just use the interface the provided us. each of these packages may have their own way to handle navigation. providing full answer to determine why exactly the screen in still in memory needs full code review and sure lots of debugging but i guess there are 2 possibilities. first as i said maybe the package you are using keeps the unmounted screens in memory at least for a while for some reason. the 2nd is a common react community issue which is Unmounted component still in memory which you can check at: https://github.com/facebook/react/issues/16138
And at last lets answer the question:
how do i keep checkboxes state even with navigating back and losing component containing their state?
This doesn't have just one way to that but simple and short answer is move your state out of the that component, e.g move it out to the parent component or a global variable.
to make it more clear lets explain like this: imagine screen A is always mounted, then you go in B and there you can see some checkboxes and you can modify the states. if the state is handled completely inside B, if you navigate back from screen B to A you lose all changes because B is now unmounted. so what you should do it to put checkboxes states in A screen then pass the values down to B. and when modifying the values, you modify A state. so when B gets unmounted all changes are persistant because you have them in A.
other approached exists as well, you can create a global singleton object named globalState. then put values needed to share between multiple screens there. if you prefer redux or mobx you can use them. one of their usages is when you have some data that you need to share between mutiple screens, these data are independent from where you are at and will persist.
This explanation is from official react-navigation documentation:
Consider a stack navigator with screens A and B. After navigating to
A, its componentDidMount is called. When pushing B, its
componentDidMount is also called, but A remains mounted on the stack
and its componentWillUnmount is therefore not called.
When going back from B to A, componentWillUnmount of B is called, but
componentDidMount of A is not because A remained mounted the whole
time.
https://reactnavigation.org/docs/navigation-lifecycle/#example-scenario
Your MyScreen screen is equivalent to screen B from the example, which means you can expect your screen to stay mounted if you navigate forward, but not backwards.
Its simple, just add a keyExtractor to your SectionList component, which would uniquely identify each checkbox, so that react knows which one to re-render on update.
You'll want to use AsyncStorage to persist data to the device. State variables will be cleared any time the component unmounts.
AsyncStorage docs:
https://react-native-community.github.io/asaync-storage/
import AsyncStorage from '#react-native-community/async-storage';
//You can only store string values so convert objects to strings:
const storeData = async (value) => {
try {
const jsonValue = JSON.stringify(value)
await AsyncStorage.setItem('#storage_Key', jsonValue)
} catch (e) {
// saving error
}
}
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('#storage_Key')
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch(e) {
// error reading value
}
}
UPDATE -
State is not being persisted due to the nature of React Component lifecycles. Specifically, when you navigate away from a screen the lifecycle method componentWillUnmount is called.
Here's an excerpt from the docs:
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount().
...Once a component instance is unmounted, it will never be mounted again.
This means any values stored in state will be destroyed as well and upon navigating back to the screen ComponentDidMount will be called which is where you may want to assign persisted values back to state.
Two possible approaches aside from AsyncStorage that may work for some use cases to persist data across screens is using Context or a singleton.

v-navigation-drawer drops into a runaway loop on window resize

First, let me say that the v-navigation-drawer works as intended, i.e.:
On clicking the hamburger menu the TOGGLE_DRAWER mutation is committed, and it toggles open/closed, updating the state.
On window resize it opens/closes at a designated breakpoint
So it works.
BUT the window resize does not properly toggle the mutation and I keep getting a Vuex mutation error when I resize the window:
I understand why I'm getting this error - the $store.state.ui.drawer is being modified outside of the mutator (it's the v-navigation-drawer's v-model):
<v-navigation-drawer
v-model="$store.state.ui.drawer"
app
clipped
>
I get it's bad form to bind the state to the v-model. But when I try to make a drawer computed property with a get() and set() method that properly gets/commits a mutation, the browser crashes (presumably because the set method triggers an endless loop of commits toggling drawer true/false into infinity):
computed: {
drawer: {
get () {
return this.$store.state.ui.drawer
},
set () {
this.$store.commit('TOGGLE_DRAWER') // <--crashes the browser
}
}
}
I've searched endlessly for a solution to this problem. It's bugging me even though it visually appears to be working.
I've considered running the v-navigation-drawer in stateless mode and handling all the window resize events and state updates manually. I've also considered disabling 'Strict' mode in Vuex (which would hide the errors). But the former is a lot more complexity and the latter is a bandaid that costs me debugging insight in development.
This sounds like a perfect candidate for Lodash's debounce function. If you need to stick with using setter/getter while applying this effect, have a look at this post; otherwise, this one for sequential event subscription on any of the lifecycle hooks.
After spending some time with this, I think I have a solution. Wanted to share for anyone else that may be facing the same issue with VNavigationDrawer using Vuex state to control visibility.
The #input event passes a val parameter, which includes the state of the drawer after the window resizes. I created a new action that is called by the below function:
<v-navigation-drawer
:value="$store.state.ui.drawer"
app
clipped
#input="updateDrawer($event)"
>
Here is the action being dispatched:
methods: {
updateDrawer(event) {
if (event !== this.drawer) { // avoids dispatching duplicate actions; checks for unique window resize event
this.$store.dispatch('updateDrawer',event)
}
}
},
And the action commits the new val to the Vuex store.
Basically, the input event is able to watch for updates to the drawer, and subsequently update the drawer state if it's necessary.
You'll also see above that I stubbornly accepted using :value as the docs suggest, even though I think this should be controlled by a v-model.
Seems to be working - with the right events called and the state being updated appropriately.

Vuetify and require.js: How do I show a dynamic component?

I am creating a tab component that loads its v-tab-item components dynamically, given an array of configuration objects that consist of tabName, id, and tabContent which is a resource location for the component. I have it successfully loading the components. However, they don't actually initialize (or run their created() methods) until I switch tabs. I just get empty tabs with the correct labels. Using the DOM inspector initially shows just <componentId></componentId>, and then when I switch tabs, those tags are replaced with all of the component's content.
How do I get the dynamic components to initialize as soon as they are loaded?
EDIT: I created a CodePen here:
https://codepen.io/sgarfio/project/editor/DKgQON
But as this is my first CodePen, I haven't yet figured out how to reference other files in the project (i.e. what to set tabContent to so that require.js can load them up). I'm seeing "Access is denied" in the console, which makes it sound like it found the files but isn't allowed to access them, which is weird because all the files belong to the same project. So my CodePen doesn't even work as well as my actual project. But maybe it will help someone understand what I'm trying to do.
Also, after poking around a bit more, I found this:
http://michaelnthiessen.com/force-re-render/
that says I should change the key on the component and that will force the component to re-render. I also found this:
https://v2.vuejs.org/v2/guide/components-dynamic-async.html
Which has a pretty good example of what I'm trying to do, but it doesn't force the async component to initialize in the first place. That's what I need the async components to do - they don't initialize until I switch tabs. In fact they don't even show up in the network calls. Vue is simply generating a placeholder for them.
I got it working! What I ended up doing was to emit an event from the code that loads the async components to indicate that that component was loaded. The listener for that event keeps a count of how many components have been loaded (it already knows how many there should be), and as soon as it receives the right number of these events, it changes the value of this.active (v-model value for the v-tabs component, which indicates which tab is currently active) to "0". I tried this because as I noted before, the async components were loading/rendering whenever I switched tabs. I also have prev/next buttons to set this.active, and today I noticed that if I used the "next" button instead of clicking on a tab, it would load the async components but not advance the tab. I had already figured out how to emit an event from the loading code, so all I had to do at that point was capture the number of loaded components and then manipulate this.active.
I might try to update my CodePen to reflect this, and if I do I'll come back and comment accordingly. For now, here's a sample of what I ended up with. I'm still adding things to make it more robust (e.g. in case the configuration object contains a non-existent component URL), but this is the basic gist of it.
created: function() {
this.$on("componentLoaded", () => {
this.numTabsInitialized++;
if(this.numTabsInitialized == this.numTabs) {
// All tabs loaded; update active to force them to load
this.active = "0";
}
})
},
methods: {
loadComponent: function(config) {
var id = config.id;
var compPath = config.tabContent;
var self = this;
require([compPath], function(comp) {
Vue.component(id, comp);
self.$emit("componentLoaded");
});
}
}

Reflux setState with a Callback

EDIT AGAIN: Opened an issue with Reflux here: https://github.com/reflux/refluxjs/issues/544
EDIT: Reflux setState does not provide any callback for setState. They require you to use the component lifecycle methods to ensure the state is set prior to running any code. If you ever need to use the reflux setState outside of a component, where you do not have lifecycle methods, you will not be guaranteed the state is set. This is due to how Reflux does their setState. It loops all listening components and calls those components' setState methods. If Reflux were refactored to wait until all the listening components' setState calls complete then call a callback passed into its own setState method, that may work, but it would likely require a large rework of Reflux. I have started using a singleton class to manage some of these variables, as they are fully outside the component lifecycle.
Can you use setState with a callback in ReactNative or is that only in React? I'm using the below syntax and the first debugger is hit, but the second debugger and console log never get hit.
EDIT: After digging some more, it seems this does not occur when using setting the state directly, but only when running it through a reflux store and/or not using a component.
See snack here: https://snack.expo.io/S1dm3eFoM
debugger
this.setState(
params,
() => {
debugger
console.log("CALLIN IT BACK")
}
)
I'm the creator of Reflux's ES6 styled stores/component hookups. Hopefully I can shed some light on this for you.
Here's the important points:
1) Reflux sets its store state immediately upon setState calls.
Reflux's store state doesn't have the same problems as React and doesn't need React's workaround (callback). You are guaranteed that your change is immediately reflected in the store's state, that's why there is not a callback. The very next line of code will reflect the store's new state.
tl;dr, no workaround is required.
// in Reflux stores this works
this.setState({foo:'foo'});
console.log(this.state.foo === 'foo') // true
this.setState({foo:'bar'});
console.log(this.state.foo === 'bar') // true
2) Stores can never depend upon components!
The idea that the setState would give a callback about when the dependent components have all updated their state is a major violation of the single most fundamental of all flux principles: 1 way data flow.
If your store requires knowledge about whether or not components are doing something then you are already doing it wrong, and all the problems you are experiencing are XY problems of fundamentally not following flux in the first place. 1-way data flow is a main flux principle.
And that principle exists for good reason. Flux doesn't require 1:1 mapping of store state properties to component state properties. You can map anything to anything, or even just use the store's state for the building blocks of how you will run your own logic to create completely new state properties on the components. For example having loaded and transitioned as separate properties in store state, but mapping to a loadedAndTransitioned property in one component, and a notLoadedOrTransitioned in another component via your own custom logic. That's a hugely powerful part of flux. But your suggestion would pretty much destroy all that, since Reflux can't map people's custom logic.
1-way data flow must be maintained; Store's must operate the same independently of what components utilize them. Without this, the power of flux falls apart!
Store's listen to actions, components listen to stores, actions are called from wherever. All flux-based data flows from action -> store -> component only.
I've checked the library for the refluxjs and the problem and the workaround are as mentioned below.
Problem
The library provides with a new instance of the setState which is not exactly similar to ReactJS setState, which omits the callback as mentioned in their code below.
/dist/reflux.js
proto.setState = function (obj) {
// Object.assign(this.state, obj); // later turn this to Object.assign and remove loop once support is good enough
for (var key in obj) {
this.state[key] = obj[key];
}
// if there's an id (i.e. it's being tracked by the global state) then make sure to update the global state
if (this.id) {
Reflux.GlobalState[this.id] = this.state;
}
// trigger, because any component it's attached to is listening and will merge the store state into its own on a store trigger
this.trigger(obj);
};
Also as mentioned here in the docs
That store will store its state on a this.state property, and mutate its state via this.setState() in a way that is extremely similar to React classes themselves.
WorkAround
The library provides with the listener functions, which provide us with the callbacks of the setState obj of the ReactJS as mentioned in the below snippet.
/dist/reflux.js
componentDidMount: function() {
var me = this;
_.extend(me, ListenerMethods);
this.listenTo(listenable, function(v) {
me.setState(_.object([key],[v]));
});
},
You can use them in the following way
this.listenTo(action, callback)
Hope it clears the doubts
Edit:
Usage as per the docs
To listen inside of the store
constructor()
{
super();
this.state = {count: 0};
this.listenTo(increment, this.incrementItUp);
}
incrementItUp()
{
var newCount = this.state.count + 1;
this.setState({count: newCount});
}
To listen outside of the store anywhere
// listen directly to an action
myActions.actionName.listen(myCallbackFunc);
// listen to a child action
myActions.load.completed.listen(myCallbackFunc);
Here's the link to the snack with working callbacks based on Promises