I currently have an asynchronous connectWallet function embedded within a React component (Modal Form) that I want to make global/available for multiple components.
Unfortunately, the function itself references jotai atoms that exist within the Modal Form.
export function ConnectWalletModal({ onClose }: IConnectWalletModalProps) {
const [, setWalletAddress] = useAtom(walletAddressAtom);
const [, setWalletBalance] = useAtom(walletBalanceAtom);
const [isWalletConnected, setIsWalletConnected] = useAtom(
isWalletConnectedAtom
);
const connectWallet = async () => {
try {
//CONNECTION LOGIC...
//Jotai
setWalletAddress(accounts[0]);
setWalletBalance(+bal);
setIsWalletConnecting(false);
setIsWalletConnected(true);
onClose();
} catch (error) {}
};
I feel like the easiest way is to export the connectWallet function outside of the component. I won't have access to the jotai atoms but maybe I can add an "onConnectSuccessful" callback function to the connectWallet arguments.
Then wherever I call connectWallet, I can pass an extra argument which will be a function that accepts the address and balance as arguments and sets the jotai state.
Problem is that I am not sure how to structure this syntax...
Related
I am using the useIsDirty hook in two components, CustomCodeEditor and EditorFooter, to track whether the code in the Editor has been modified. The hook returns an isDirty state and a setIsDirty function to update it. When I call setIsDirty(true) in the CustomCodeEditor component, the state is updated, but when I call setIsDirty(false) in the EditorFooter component, it doesn't seem to update the isDirty state. I believe this is because the EditorFooter component does not have access to the updated state. Anyone, please help me with this.
useIsDirty:
import { useEffect, useState } from "react"
const useIsDirty = () => {
const [isDirty, setIsDirty] = useState(false)
useEffect(() => {
const handleBeforeUnload = (event) => {
if (isDirty) {
event.preventDefault()
event.returnValue = ""
alert("You have unsaved changes, are you sure you want to leave?")
}
}
console.log("Diryt:", isDirty)
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [isDirty])
return { isDirty, setIsDirty }
}
export default useIsDirty
CustomCodeEditor
import Editor from "#monaco-editor/react"
import useIsDirty from "../../hooks/useIsDirty"
const CustomCodeEditor = () => {
const { isDirty, setIsDirty } = useIsDirty()
console.log("isDirty:", isDirty)
return (
<div className="bg-[#1e1e1e] h-full">
<Editor
onChange={(value) => {
updateCode(value || "")
setIsDirty(true) // updating state
}}
/>
</div>
)
}
export default CustomCodeEditor
EditorFooter
import Button from "../reusable/Button"
const EditorFooter = () => {
const { setIsDirty } = useIsDirty()
const handleSave = async () => {
setIsDirty(false)
}
return (
<div>
<Button
onClick={handleSave}
>
Save
</Button>
<Button
onClick={handleSave}
>
Submit
</Button>
</div>
)
}
export default EditorFooter
Hooks are not singleton instances.. when you use useIsDirty somewhere.. it always create new instance, with unrelated states to other ones. If you want to share this state you need to use Context
const IsDirtyContext = createContext(undefined);
const IsDirtyProvider = ({ children }): ReactElement => {
const [isDirty, setIsDirty] = useState(false)
return <IsDirtyContext.Provider value={{isDirty, setIsDirty}}>{children}</IsDirtyContext.Provider>;
};
and then you should wrap your commponent tree where you wanna access it with IsDirtyProvider
after that, you can even create your custom hook that will just return that context:
const useIsDirty = () => {
return useContext(IsDirtyContext)
}
Looking at your question, it looks like you are trying to use the same state in both components. However, the state doesn't work like that. A new instance is created whenever you make a call to useIsDirty from a different component.
If you want to use the state value across two components. You can do that using one of the following ways.
1 - Use a parent and child hierarchy.
Steps
Create a parent component and wrap the two components inside the parent component.
Manage the state in the parent component and pass it using props to the child component.
Create a function in child components that will execute a function from the parent component. The parent component function will hold the code to update the state based on whatever value you receive from the child component.
Now you should be able to share your state between both components.
2 - Use the context api.
If you are not familiar with what context api is, below is a brief explanation.
Context api helps you share data between components, without the need of passing them as a prop to each and every component.
You can use createContext and useContext hooks from context api.
createContext is used to create a new context provider.
useContext hook is used to manage the state globally.
You can get the value from context using this function.
Whenever the state is updated the value will be reflected globally.
Note - Any component that needs to use the value inside the useContext should be wrapped inside the useContext provider component.
Steps to create a context provider.
To create a context you just need to use the react hook createContext
Create a context using below code
const isDirtyContext = createContext();
Wrap your components in the context provider
import {IsDirtyContext} from './path/filename'
<IsDirtyContext.Provider value={[isDirty, setIsDirty]}>{children}</IsDirtyContext.Provider>
If your context is in a separate file, then you can import it into any child component using the import statement.
import {IsDirtyContext} from './path/filename'
Use the context
const [isDirty] = useContext(IsDirtyContext);
Now the isDirty state value is available globally in all components.
Hope this information helps you. Please upvote if this helps you understand and solve the problem.
I have a scenario where I am forced to call a trigger method to show a modal from two different places, one using a hotkey combination and another by clicking on a toolbar button. In order to do so I have the following code, where I call the triggerCustomLinkModal to set the state but then I am hit with the Invalid Hook call error.
import { useState, useCallback, useEffect } from "react"
import { Dialog } from "#blueprintjs/core"
const useLocalState = () => {
const [isShown, setIsShown] = useState(false)
const setState = useCallback((state) => {
setIsShown(state)
})
const getState = useCallback(() => {
return isShown
})
return {
setState,
getState
}
}
export const CustomLinkModalUI = () => {
const { getState } = useLocalState()
return (
<>
<Dialog isOpen={getState()} />
</>
)
}
export const triggerCustomLinkModal = () => {
const { setState } = useLocalState()
setState()
}
Expanding from Chris answer in the comments ( You can't use hooks outside React components. -> so you can't call useLocalState() inside triggerCustomLinkModal since triggerCustomLinkModal is not a React component ):
You don't really need the useCallback hook or even the functions itself. Aaccording to react docs :
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
This also means that using useCallback hook to set a state it doesn't really make sense (because useCallback role is just to return a memoized callback)
What you basically need is a state set up in the closest parrent component and pass the setIsShown as a prop as well as the isShown function.
Your current implementation, even if it weren't for the error, it wouldn't refer to the same state since on each useLocalState() you are initializing a fresh new state (so you are not pointing to the same state in CustomLinkModalUI and triggerCustomLinkModal)
I am new to React so please forgive me if this issue is entry-level to you.
Basically, I want to do the thing as the title goes,
here I have a function designed for this purpose:
function saveAndUpdate() {
// data processing and wrap up the finalized data, lets call it newData
useEffect(() => {
async function loadData() {
try {
await axios.put(API/updateEntry,{objToUpdate: newData}).then(() => {
const { loading, error, data } = axios.get(API/dataForTheTable)
callback(data);
});
} catch (error) {
console.error(error);
}
}
loadData();
}, []);
}
callback is a function in parent component in order to update the state related to the data immediately after the data is updated.
const handleCallback = (data) => {
setInfo(data);
}
And by running this, I get the classic error which I still do not fully understand:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
Any help will be appreciated, thank you!
Seems that what you're doing is creating a hook (saveAndUpdate) and trying to access to It somewhere (for example, inside another hook or function).
Althought, a hook to update data (with axios.put, where you need to pass an argument; the data for the update) is not a good approach, as an UseEffect hook will run when the component is mounted, not when the user clicks on a button or whatever. For example, it would be better if you use this for gathering data (get request) from your backend when the component mounts.
Anyway this is a way you could do what you want to achieve, using the useCallback. I've created a button that fires the function and pass some data (just you can see the expected behaviour):
In the saveAndUpdate hook (name it as useSaveAndUpdate, just a convention for hooks names):
import { useEffect, useState } from "react";
export default function useSaveAndUpdate(dataUpdate) {
const [data, setData] = useState(null);
const [updating, setUpdating] = useState(false);
useEffect(() => {
if (updating) {
const req = function () {
// the put request (dataUpdate will be the data we use for the request)...
// for this i'm just going to set data to the argument we've passed
// the idea is to setData to the returned data from the server
setData(dataUpdate);
setUpdating(false);
// setting updating to false, if there's a re-render it won't do the request again
};
req();
}
}, [updating, dataUpdate]);
return [data, updating, setUpdating];
}
In the parent component:
import { useState, useCallback } from "react";
import useSaveAndUpdate from './useSaveAndUpdate'
const parent = () => {
const [updateTarget,setUpdateTarget = useState(null)
const [data,updating,setUpdating] = useSaveAndUpdate(updateTarget)
const updateFunction = useCallback((updateData) => {
setUpdating(true)
//
// ^ Setting updating to true will fire the request inside useSaveAndUpdate when re-render
//
setUpdateTarget(updateData)
// Setting the data for the update (this data will be used in the put request)
},[setUpdating,setUpdateTarget])
return (
<h3>{loading?'loading...':'not loading'}</h3>
<h3>{data?data.name:'data is null'}</h3>
{/*data will be updated when button clicked, so if it's a table just do data.map()...*/}
<button onClick={() => updateFunction({name:'jhon'})}>Click me to update</button>
)
}
So at first render, It will outpout that It's not loading and data is null.
When click It'll say loading for a sec (the time the request lasts), and the data will be displayed ('jhon').
A small codesandbox so you can see how it works.
I have a function like so:
export const fireView = (prop1, prop2) => WrappedComponent =>
class extends Component {
//stuff in here
}
then a hoc like this:
export const withFireView = (prop1, prop2) =>
fireView(prop1, prop2)
but now I want to put this function inside compose as I need to call it with mapStateToProps
so I did this:
compose (
connect(mapStateToProps),
fireView
)
but it breaks because You must pass a component to the function returned by connect
so I then I get rid of the arguments in the fireview function and I think this will work but then all the arguments are now undefined inside the function as I've not passed them
Is there any way to do something like this:
compose (
connect(mapStateToProps),
fireView(arg1, arg2)
)
but obviously, they are not defined there if that makes sense.
Here is a full working example:
var Component = React.Component;
var render = ReactDOM.render;
var Provider = ReactRedux.Provider;
var connect = ReactRedux.connect;
var createStore = Redux.createStore;
var compose = Redux.compose;
const reducer = () => {return {}};
const mapStateToProps = () => {
return {};
};
const wrapped = (props) => {
return <div>{props.prop1} {props.prop2}</div>;
}
const fireView = (prop1, prop2) => WrappedComponent => {
return class extends Component {
render() {
return <WrappedComponent prop1={prop1} prop2={prop2} />;
}
}
}
const withFireView = (prop1, prop2) => fireView(prop1, prop2);
const withFireViewAndConnect = (arg1, arg2) => compose(connect(mapStateToProps), withFireView(arg1, arg2));
const App = withFireViewAndConnect('some', 'arg')(wrapped);
render(<Provider store={createStore(reducer)}><App /></Provider>, document.getElementById('demo'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.13.0/polyfill.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/6.0.0/react-redux.js"></script>
<div id="demo"></div>
I had a similar requirement and ended up on this page. I supposed we were not able to understand the use case. I will try to summarise it and also provide an answer to it.
Explanation
We can use HOC (Higher Order Components) in React to abstract out common functionality. In React docs, we can see the 'with' notation to use these HOCs. I have created a few on my app as well.
withSplashScreenRedirect, withHistoryObject
All these HOCs that I created takes a Component (WrappedComponent) and returns a Component. (No additional Arguments required).
const EnhancedComponent = higherOrderComponent(WrappedComponent);
I would generally have around 2 to 3 HOCs on each Screen level Component. Writing these HOCs as functions returning other functions look ugly. Redux provides a useful compose function. Compose helps us to chain these HOCs in a more readable manner.
From compose docs.
All compose does is let you write deeply nested function transformations without the rightward drift of the code. Don't give it too much credit!
Soon I wanted to create another HOC that would standardise my API call logic and API call handling logic. In this case, I had to send two additional functions to my HOC (along with the Wrapped Component).
withApiCallHandler(WrappedComponent, makeApiCall, handleApiResponse)
Passing props to HOC is not a big deal. React Docs have a good example explaining how to do it. However, adding such an HOC to compose chain is not so straightforward. I guess that is what OP was trying to ask here.
Answer
I wasn't able to find a way to chain such a HOC with compose. Maybe it's not possible. The best solution would be to initialise the withApiCallHandler Component separately and then chain it with compose.
Instead of
const ComposedScreenComponent = compose(
withHOC1,
withHOC2,
withHOC3(arg1, arg2)
)(ScreenComponent);
Do this
const withHOC3AndArgs = (WrappedComponent) =>
withHOC3(WrappedComponent, arg1, arg2);
const ComposedScreenComponent = compose(
withHOC1,
withHOC2,
withHOC3AndArgs(arg1, arg2)
)(ScreenComponent);
This is not the best answer to the question, but if someone is stuck then this solution will surely be a good workaround.
You can make withFireView return a function instead. All will work.
export const withFireView = (prop1, prop2) => () => fireView(prop1, prop2)
I am using this starter kit https://github.com/davezuko/react-redux-starter-kit and am following some tutorials at the same time, but the style of this codebase is slightly more advanced/different than the tutorials I am watching. I am just a little lost with one thing.
HomeView.js - This is just a view that is used in the router, there are higher level components like Root elsewhere I don't think I need to share that, if I do let me know, but it's all in the github link provided above.
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { searchListing } from '../../redux/modules/search'
export class HomeView extends React.Component {
componentDidMount () {
console.log(this.props)
}
render () {
return (
<main onClick={this.props.searchListing}>
<NavBar search={this.props.search} />
<Hero/>
<FilterBar/>
<Listings/>
<Footer/>
</main>
)
}
}
I am using connect() and passing in mapStateToProps to tell the HomeView component about the state. I am also telling it about my searchListing function that is an action which returns a type and payload.
export const searchListing = (value) => {
console.log(value)
return {
type: SEARCH_LISTINGS,
payload: value
}
}
Obviously when I call the method inside the connect() I am passing in an empty object searchListing: () => searchListing({})
const mapStateToProps = (state) => {
return {
search: { city: state.search }
}
}
export default connect((mapStateToProps), { searchListing: () => searchListing({}) })(HomeView)
This is where I am stuck, I am trying to take the pattern from the repo, which they just pass 1, I think anytime that action is created the logic is just add 1 there is no new information passed from the component.
What I am trying to accomplish is input search into a form and from the component pass the users query into the action payload, then the reducer, then update the new state with the query. I hope that is the right idea.
So if in the example the value of 1 is hardcoded and passed into the connect() method, how can I make it so that I am updating value from the component dynamically? Is this even the right thinking?
You almost got it right. Just modify the connect function to pass the action you want to call directly:
const mapStateToProps = (state) => ({
search: { city: state.search }
});
export default connect((mapStateToProps), {
searchListing
})(HomeView);
Then you may use this action with this.props.searchListing(stringToSearch) where stringToSearch is a variable containing the input value.
Notice : You don't seem to currently retrieve the user query. You may need to retrieve it first and then pass it to the searchListing action.
If you need to call a function method, use dispatch.
import { searchListing } from '../../redux/modules/search';
const mapDispatchToProps = (dispatch) => ({
searchListing: () => {
dispatch(searchListing());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(HomeView);
Then, you have made the function a prop, use it with searchListing.