Transform a component into a hook that provide component and handers - reactjs

I'would like to expose the component, the state of the component within some handlers to interact with it, but I'm not aware of the caveats this might have, and never seen something like this. Can you provide some guide here?
Is a pattern a never saw, anyways, seems to be valid to me.
The hook:
const useEmptyCartModal: useEmptyCartModalHook = ( {
testID,
orderID,
} ) => {
const { t } = useTranslation()
const [ isEmptyCartModalOpen, setIsEmptyCartModalOpen ] = useState( false )
const router = useRouter()
const navigateToShop = () => router.push( ShopRoutes.getShopUrl() )
const [ abandonOrder, { loading: abandonOrderLoading, error: abandonOrderError } ] = useAbandonOrderMutation()
const handleOnCompleteEmptyCart = () => {
setIsEmptyCartModalOpen( false )
navigateToShop()
}
const handleEmptyCart = () => orderID && abandonOrder( {
variables: { input: { orderID } },
onCompleted: handleOnCompleteEmptyCart,
} )
const openEmptyCartModal = () => setIsEmptyCartModalOpen( true )
const emptyCartModal = <EmptyCartModal
isOpen={ isEmptyCartModalOpen }
setOpen={ setIsEmptyCartModalOpen }
submitting={ abandonOrderLoading }
testID={ testID }
onSubmitEmptyCart={ handleEmptyCart }
/>
const emptyCartError = abandonOrderError ? t( ComponentsCartDetailsI18n.MESSAGES_EMPTY_CART_ERROR_TEXT ) : undefined
return( {
emptyCartModal,
emptyCartError,
openEmptyCartModal,
} )
}
export { useEmptyCartModal as default }
In the component I would use the Component and the component that will interact with it:
const { openEmptyCartModal, emptyCartModal, emptyCartError } = useEmptyCartModal( {
testID: getTestID( 'EmptyCartModal' ),
orderID: cart.id,
} )

I don't think that doing this way is necessarily a problem, but I think that if your are dealing with a modal component is better to make an abstraction of the modal component and change the contents of it programmatically.
You can do this by creating a "global" component that acts like a modal recipient and use a State handler such as redux or react contexts to manipulate the contents and visibility of the modal box then you would be able to put anything in the modal recipient the emptyCart component and whatever else you need.
There is nothing wrong with the way you're doing it just doesn't scale well, but if you're not going to have a lot of different types of modal boxes than your solution is good enough.

Related

React Context - infinite useEffect loop

I am aware of the multiple similar questions regarding this type of error both on articles and on SO as well but none of them have applied or worked for me up until this point.
So, I am using the React Context API to keep track of a state shared between multiple components:
// imports removed because irrelevant
interface IMarkedPlacesContext {
markedPlaces: Place[]
setMarkedPlaces: React.Dispatch<React.SetStateAction<Place[]>>
markPlaces: (places : Place[]) => Promise<void>
}
interface Props {
children: React.ReactNode
}
const MarkedPlacesContext = createContext<IMarkedPlacesContext>(
{} as IMarkedPlacesContext
)
export function MarkedPlacesContextProvider({ children }: Props) {
const [markedPlaces, setMarkedPlaces] = useState<Place[]>([])
const markPlaces = useCallback( async (places: Place[]) => {
const placesWithRatings : Place[] = await Promise.all(places.map(async (place) => {
const placeRating = await FirestorePlaceRatingService.getPlaceRating(place.placeId)
if (placeRating) {
return({
...place,
rating: placeRating.sumRating/placeRating.reviewCount,
reviewCount: placeRating.reviewCount
})
} else {
return(place)
}
}))
setMarkedPlaces(placesWithRatings)
}, [])
return (
<MarkedPlacesContext.Provider
value={{
markedPlaces,
setMarkedPlaces,
markPlaces,
}}
>
{children}
</MarkedPlacesContext.Provider>
)
}
export function useMarkedPlaces() {
return useContext(MarkedPlacesContext)
}
This component sits on the root level of my app. The function to take note of here is markPlaces which fetches the corresponding rating (if it exists) from my Firestore backend, otherwise, it just gives back the original place object with rating as undefined.
Now the problem occurs when I try to use markPlaces in a useEffect in one of my components:
const PlaceListScreen = () => {
const { markedPlaces, markPlaces, setMarkedPlaces } = useMarkedPlaces()
const { setPlaceInfo, focusPlace } = usePlace()
const { navigate } =
useNavigation<StackNavigationProp<BottomSheetStackParams>>()
useEffect(() => {
if (focusPlace) navigate('PlaceReview')
}, [focusPlace])
// INFINITE LOOP OCCURS HERE! :(
useEffect(() => {
console.log('marking..')
markPlaces(markedPlaces)
}, [])
const handlePressPlace = (place: Place) => {
setPlaceInfo(place)
navigate('PlaceReview')
}
const renderRating = (placeRating: number | undefined) => (
<Subheading style={{flex: 0.1, paddingLeft: 15}}>
{placeRating ? placeRating.toFixed(1) : '-' }
</Subheading>
)
const renderPlaceItem = (place: Place) => (
<TouchableOpacity onPress={() => handlePressPlace(place)}>
<List.Item
title={place.name}
description={place.formattedAddress}
right={() => renderRating(place.rating)}
/>
</TouchableOpacity>
)
return (
<BottomSheetFlatList
data={markedPlaces}
keyExtractor={(place) => place.placeId}
renderItem={(place) => renderPlaceItem(place.item)}
/>
)
}
export default PlaceListScreen
I do not understand why calling markPlaces in this manner causes an infinite loop. Is it something to do with the way I pass in markedPlaces as a param? Or is it something to do with the way I have structured my context?
Any help would be much appreciated :)
UPDATE
I have a hacky work-around of the issue, what I did was to make a local state called places held on the component PlaceListScreen which essentially acts as a reflection of the main value store in the MarkedRestaurants Context. Instead of changing the state held within the context, I change the local state. However, this is an extremely ugly solution which I could see definitely causing problems in the future.
I've been mulling over why having the value stored in context is the cause of the infinite re-renders, but I'm still failing to see why. If someone could help me figure out why this is happening and a more appropriate solution for it that would be awesome.

How to call react function from external JavaScript file

I have read this post [ https://brettdewoody.com/accessing-component-methods-and-state-from-outside-react/ ]
But I don't understand.
that is not working on my source code.
it's my tsx file
declare global {
interface Window {
fn_test(): void;
childComponent: HTMLDivElement; <-- what the... ref type????
}
}
export default function Contact(): React.ReactElement {
....
function file_input_html( i:number ): React.ReactElement {
return (
<form id={`frm_write_file_${i}`} .... </form>
)
}
....
return (
<div ref={(childComponent) => {window.childComponent = childComponent}}>
....
)
it's my external javascript file
function fn_test(){
window.childComponent.file_input_html(3)
var element = document.getElementById("ifrm_write_file");
// element.value = "mystyle";
}
How can i call file_input_html function?
plase help me ...
You have some logic here that doesn't completely make sense.
In your class, you define file_input_html, which returns a component.
Then, in fn_test, you call attempt to call that function (which doesn't work -- I'll address that in a minute), but you don't do anything with the output.
The article that you linked to tells you how to get a ref to a component (eg the div in this case) -- not the actual Contact, which doesn't have a property named file_input_html anyway -- that's just a function you declared inside its scope.
What I'm assuming you want to happen (based on the code you shared) is for your external javascript file to be able to tell your component to render a form with a certain ID and then be able to get a reference to it. Here's an example of how to do this (it's a little convoluted, but it's a funny situation):
const { useState } = React
const App = (props) => {
const [formId, setFormId] = useState(2)
useEffect(() => {
window.alterFormId = setFormId
},[])
return (<div id={"form" + formId} ref={(ourComponent) => {window.ourComponent = ourComponent}}>
Text {formId}
</div>);
}
setTimeout(() => {
window.alterFormId(8);
setTimeout(() => {
console.log(window.ourComponent)
window.ourComponent.innerText = "Test"
}, 20)
}, 1000)
ReactDOM.render(<App />,
document.getElementById("root"))
What's happening here:
In useEffect, I set alterFormId on window so that it can be used outside of the React files
Using the technique you linked to, I get a ref to the div that's created. Note that I'm setting the ID here as well, based on the state of formId
The setTimeout function at the end tests all this:
a) It waits until the first render (the first setTimeout) and then calls alterFormId
b) Then, it waits again (just 20ms) so that the next run loop has happened and the React component has re-rendered, with the new formId and reference
c) From there, it calls a method on the div just to prove that the reference works.
I'm not exactly sure of your use case for all this and there are probably easier ways to architect things to avoid these issues, but this should get you started.
안녕하세요. 자바스크립트로 흐름만 알려드리겠습니다
아래 코드들을 참고해보세요.
iframe간 통신은
window.postMessage API와
window.addEventListener('message', handler) 메시지 수신 이벤트 리스너 로 구현할 수있습니다. 보안관련해서도 방어로직이 몇줄 필요합니다(origin 체크 등)
in parent
import React from 'react';
export function Parent () {
const childRef = React.useRef(null);
const handleMessage = (ev) => {
// 방어로직들
if (check ev.origin, check ev.source, ....) {
return false;
}
console.log('handleMessage(parent)', ev)
}
React.useEffect(() => {
window.addEventListener('message', handleMessage);
// clean memory
return () => {
window.removeEventListener('message', handleMessage);
}
})
return (
<div>
<iframe ref="childRef" src="child_src" id="iframe"></iframe>
</div>
)
}
in child
import React from 'react';
export function Iframe () {
const handleMessage = (ev) => {
console.log('handleMessage(child)', ev)
}
const messagePush = () => {
window.parent.postMessage({ foo: 'bar' }, 'host:port')
}
React.useEffect(() => {
window.addEventListener('message', handleMessage);
// clean memory
return () => {
window.removeEventListener('message', handleMessage);
}
})
return (
<div>
<button onClick={messagePush}>Push message</button>
</div>
)
}

JSX Element does not have any construct or call signatures

I am trying to add Application Insights in my ReactJS Application. I changed the JS code that is provided on the GitHub Demo to TypeScript.. now I have
class TelemetryProvider extends Component<any, any> {
state = {
initialized: false
};
componentDidMount() {
const { history } = this.props;
const { initialized } = this.state;
const AppInsightsInstrumentationKey = this.props.instrumentationKey;
if (!Boolean(initialized) && Boolean(AppInsightsInstrumentationKey) && Boolean(history)) {
ai.initialize(AppInsightsInstrumentationKey, history);
this.setState({ initialized: true });
}
this.props.after();
}
render() {
const { children } = this.props;
return (
<Fragment>
{children}
</Fragment>
);
}
}
export default withRouter(withAITracking(ai.reactPlugin, TelemetryProvider));
But when I try to import the same component <TelemetryProvider instrumentationKey="INSTRUMENTATION_KEY" after={() => { appInsights = getAppInsights() }}></Telemetry> I get an error Severity Code Description Project File Line Suppression State
(TS) JSX element type 'TelemetryProvider' does not have any construct or call signatures.
I attempted to simply // #ts-ignore, that did not work. How do I go about solving this?
Given the example above, I hit the same issue. I added the following:
let appInsights:any = getAppInsights();
<TelemetryProvider instrumentationKey={yourkeyher} after={() => { appInsights = getAppInsights() }}>after={() => { appInsights = getAppInsights() }}>
Which seem to solve the issue for me, I am now seeing results in Application Insights as expected.
I guess if you want to have the triggers etc on a different Page/Component you may wish to wrap it in your own useHook or just add something like this to the component.
let appInsights:any;
useEffect(() => {
appInsights = getAppInsights();
}, [getAppInsights])
function trackEvent() {
appInsights.trackEvent({ name: 'React - Home Page some event' });
}
Not the best answer, but it's moved me forward. Would be nice to see a simple hooks version in typescript.
Really hope it helps someone or if they have a cleaner answer.

Should I use useMemo in hooks?

I created useBanner hooks
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState(array.slice(0, yardage));
const [bannListIndex, setBannIndex] = useState(1);
return {
....
};
};
Am I doing the right thing and the props throw in useState.
It’s permissible to use useBanner.
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
when props will change here.
Will change the state in useBanner.
or is it considered anti-patterns I have to write all this in useMemo
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState([]);
const [bannListIndex, setBannIndex] = useState(1);
useMemo(() => {
setBannArr(array.slice(0, yardage));
setBannIndex(1);
}, [array, yardage]);
return {
....
};
};
Yes, custom hooks are possible in React. Here is separate document discussing custom hooks.
But exactly you sample may require additional code depending on what is your final goal.
If you want initialize state only once, when component Banner is first created, you can just do as in your first sample
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
This will work perfectly. But if props array and yardage will change, this will not be reflected in component. So props will be used only once as initial values and then will not be used in useBanner even if changed (And it doesn't matter whether you'll use useBanner or useState directly). This answer highlight this.
If you want to update inital values on each props change, you can go with useEffect like below
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr, setBannArr } = useBanner(array, yardage);
useEffect (() => {
// setBannArr should also be returned from useBanner. Or bannArr should be changed with any other suitable function returned from useBanner.
setBannArr(array.slice(0, yardage));
}, [array, yardage, setBannArr])
return (
...
);
};
In this case Banner component can control state itself and when parent component change props, state in Banner component will be reset to new props.
Here is small sample to showcase second option.

Central State Management without Redux or Mobx?

Recently I contemplated the idea of having central state management in my React apps without using Redux or Mobx, instead opting to create something similar to the application class in Android. In any event, I implemented something similar to this:
Create a store folder and a file called store.js in it whose contents are:
// State
let state = {
users: {},
value: 0
};
// Stores references to component functions
let triggers = [];
// Subscription Methods
export const subscribe = trigger => {
triggers.push(trigger);
trigger();
}
export const unsubscribe = trigger => {
let pos = -1;
for (let i in triggers) {
if (triggers[i]===trigger) {
pos = i;
break;
}
}
if (pos!==-1) {
triggers.splice(pos, 1);
}
}
// Trigger Methods
let triggerAll = () => {
for (let trigger of triggers) {
trigger();
}
}
// State Interaction Methods
export const setUser = (name, description) => {
state.users[name] = description;
triggerAll();
}
export const removeUser = name => {
if (name in state.users) {
delete state.users[name];
}
triggerAll();
}
export const getAllUsers = () => {
return state.users;
}
export const getUser = name => {
if (!(name in state.users)) {
return null;
}
return state.users[name];
}
export const getValue = () => {
return state.value;
}
export const setValue = value => {
state.value = value;
triggerAll();
}
And connecting to this store in the following manner:
// External Modules
import React, { Component } from 'react';
import {Box, Text, Heading} from 'grommet';
// Store
import {subscribe, unsubscribe, getAllUsers} from '../../store/store';
class Users extends Component {
state = {
users: []
}
componentDidMount() {
subscribe(this.trigger); // push the trigger when the component mounts
}
componentWillUnmount() {
unsubscribe(this.trigger); // remove the trigger when the component is about to unmount
}
// function that gets triggered whenever state in store.js changes
trigger = () => {
let Users = getAllUsers();
let users = [];
for (let user in Users) {
users.push({
name: user,
description: Users[user]
});
}
this.setState({users});
}
render() {
return <Box align="center">
{this.state.users.map(user => {
return <Box
style={{cursor: "pointer"}}
width="500px"
background={{color: "#EBE7F3"}}
key={user.name}
round
pad="medium"
margin="medium"
onClick={() => this.props.history.push("/users/" + user.name)}>
<Heading margin={{top: "xsmall", left: "xsmall", right: "xsmall", bottom: "xsmall"}}>{user.name}</Heading>
<Text>{user.description}</Text>
</Box>
})}
</Box>;
}
}
export default Users;
Note. I've tested this pattern on a website and it works. Check it out here. And I apologize I am trying to keep the question concise for stackoverflow, I've provided a more detailed explanation of the pattern's implementation here
But anyway, my main question, what could be the possible reasons not to use this, since I assume if it was this simple, people wouldn't be using Redux or Mobx. Thanks in advance.
That's what Redux and MobX basically do, you are wrong in thinking that at their core concept they are much different. Their size and complexity came as a result of their effort to neutralize bugs and adapt to a vast variety of application cases. That's it. Although they might be approaching the task from different angles, but the central concept is just that. Maybe you should familiarize your self with what they actually do underneath.
Btw, you do not need to store redundant state in your component, if all you need is to trigger the update. You can just call forceUpdate() directly:
// function that gets triggered whenever state in store.js changes
trigger = () => {
this.forceUpdate();
}
That's similar to what Redux and MobX bindings for react do under the hood.

Resources