How to access the functional component's name from a React hook? - reactjs

I am trying to write a custom React hook, useLogging, where I would like to contextualize the log message based upon the name of the component which is doing the logging.
For example:
const Login: React.FunctionComponent<IProps> = (props) => {
log = useLogging();
log.info("Hello!")
[...]
Should produce [Login] Hello!
My custom hook, then, needs the name Login:
export const useLogger = () => {
// "this" is undefined
const loggerName = ??????
return logManager.getLogger(loggerName);
};
In the context of a class, what I'm looking for is something like this.constructor.displayName. However, a React hook does not have this set, and I cannot seem to find documentation on obtaining reference to the functional component's context.
—-
Edit: I would prefer not to pass any arguments and not to add a bunch of boiler plate. My goal is that the useLogging() function will survive component refactoring, and not rely upon the developer to provide a "correct" name.

There's a couple other ways that I can think of that you could use. The first and the simplest the one Drew suggested in the comment, which is to simply pass the logging name as an argument:
const useLogger = (name: string) => {
return logManager.getLogger(name)
}
const Login: React.FC<Props> = () => {
const log = useLogger('Login')
// ...
}
You could also obtain the name via .displayName or .name. Notice that .name refers to Function.name, which, if you're using webpack, will probably be minified in a production build so you'll end up with names like "t" or "s" etc. If you need the same name as in your component, you can assign displayName and let the hook take care of it:
const useLogger = (component: React.ComponentType<any>) => {
const name = useLogger(component.displayName || component.name);
return logManager.getLogger(name);
}
const Login: React.FC<Props> = () => {
const log = useLogger(Login)
}
Login.displayName = 'Login';
If you're okay with passing a name to useLogger, but don't want to set displayName every time, you could use something like ts-nameof which aims to give you a nameof operator like in C#:
const useLogger = (name: string) => {
return logManager.getLogger(name)
}
const Login: React.FC<Props> = () => {
const log = useLogger(nameof(Login))
// ...
}
The upside here is that the name will survive auto-renames. This requires some bundler or Babel configuration. I haven't tested how minification affects this, but there's three different flavors of ts-nameof (at the time of writing) which you can use:
a compile-time transform for the TypeScript compiler: ts-nameof
a Babel macro: ts-nameof.macro
a Babel plugin: babel-plugin-ts-nameof
Pick the first one that matches your build pipeline.
Alternatively, if the logger isn't component-specific, but module-specific, you could make a factory for the hook, and initialize it once at the top of your module:
const makeUseLogger = (name: string) => () => {
return logManager.getLogger(name)
}
// in your module
const useLogger = makeUseLogger('Module name')
const Login: React.FC<Props> = () => {
const log = useLogger()
// ...
}
As an extension of this, if the logger itself doesn't actually need to be a hook (doesn't use other hooks or need props etc.), just make a logger for your module at the top level directly:
const log = logManager.getLogger('Module name')
const Login: React.FC<Props> = () => {
log.info('hello')
}
Additionally, if you don't mind your project's directory structure leaking into a production build, you can use a webpack trick:
// webpack.config.js
module.exports = {
// ...
node: {
__filename: true
}
}
and then
const log = logManager.getLogger(__filename)
In a file whose path is /home/user/project/src/components/Login.ts, and the webpack context is /home/user/project, the __filename variable will resolve to be src/components/Login.ts.
Although, this will probably require you to create a typedef e.g. globals.d.ts where you declare the __filename global for Typescript:
declare global {
__filename: string;
}
Note: this will not work if your build target is umd.
As a side-note, technically, if you for some reason don't want to pass any args to useLogging, you could use the deprecated Function.caller property, e.g.
function useLogging() {
const caller = (useLogging.caller as React.ComponentType<any>);
const name = caller.displayName || caller.name;
console.log(name);
return logManager.getLogger(name);
}
const Login: React.FC<Props> = () => {
const log = useLogging()
// ...
}
However, that property is deprecated so you'll have to clean that up sooner or later, so don't do that in production code.

Related

Writing a TypeScript Interface for React Context, how to describe the interface/ different types to be passed as values?

I am trying to write a React Context for my application
This is a follow up from my previous question:
How to pass value to Context Provider in TypeScript?
I would please like some help describing different value types to be passed through the provider.
Overview:
I am currently trying to construct a context provider that can be used across the scope of my application in TypeScript.
It contains some useState hooks and and an asynchronous function to be passed through as values the provider to all the child components.
ResultConextProvider.tsx
export const ResultContextProvider = () => {
const [isLoading, setIsLoading] = useState<boolean>(false)
const [greenStatus, setGreenStatus] =
useState(new Set<MyEnum>());
const [redStatus, setRedStatus] =
useState(new Set<MyEnum>());
const [searchTerm, setSearchTerm] = useState<string>('')
// objects to be passed values
const greenValue = {greenStatus, setGreenStatus};
const redValue = {redStatus, setRedStatus};
const searchValue = {searchTerm, setSearchTerm};
// api function coming from tested API spec (external)
const getResults = async () => {
setIsLoading(true)
myAPI.myGet(greenStatus, redStatus).then((result) => {
setResults(result.data);
})
setIsLoading(false)
}
return (
<ResultContext.Provider value={{getResults, greenValue, redValue, searchValue}}>
{children}
</ResultContext.Provider>
}
export const useResultContext = () => useContext(ResultContext);
As you can see above, I would like to pass the getResults function, my greenValues, redValus and searchValues to all my child components, the Provider implentation will look something like this:
index.tsx
import { ResultContextProvider } from "./contexts/ResultContextProvider";
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<ResultContextProvider
<Router>
<App />
</Router>
</ResultContextProvider>
);
When it comes to writing the interface, I am struggling to understand what needs to be represented prior to my component.
So far my interface looks something like this:
ResultConextProvider.tsx
interface ContextParametersType {
greenValue: { greenStatus: Set<MyEnum>, setGreenStatus:
Dispatch<SetStateAction<Set<MyEnum>>> };
redValue: { redStatus: Set<MyEnum>, setRedStatus:
Dispatch<SetStateAction<Set<MyEnum>>> };
///INCORRECT //
// searchValue: {searchTerm: <string>(''), setSearchTerm:
Dispatch<SetStateAction<string> };
///UNSURE
// getResults : {
// What to write as interface for getResults
// }
}
I have worked out what needs to be declared for my greenValue and redValue, based on the answer to my previous question however struggling to fill out the rest of the interface specification
The Search Term:
///INCORRECT //
// searchValue: {searchTerm: (''), setSearchTerm:
Dispatch<SetStateAction };
I have tried to follow the pattern of the Enum state, particularly in declaring the state as type string and rerferecning the Dispatch<SetStateAction as the same type to change the state however this produces error
getResults
This is an asynchronous function with an Axios function inside, to be truthful i do not know where to begin within this one.
My confusion on these two steps persists when creating the ResultContext
const ResultContext = createContext<ContextParametersType>({
greenValue: {
greenStatus: new Set<FirmStatusEnum>(), setGreenStatus: () => {
}
},
redValue: {
redStatus: new Set<FirmStatusEnum>(), setRedStatus: () => {
}
},
// searchValue: {
// searchTerm: <string>(''), setSearchTerm: () => {
// }
// },
getResults()
// Not Sure how to initalsie this?
});
Does this make sense? Am I understanding the issues here correctly?
I would really like some help in understanding how to configure the context interfaces for this.
It is essentially a typescript adaptation of this tutorial
Here:
Thanks, please let me know if I need to provide clarity

Using React Context in a custom hook always returns undefined

I'm trying to create a custom hook which will eventually be packaged up on NPM and used internally on projects in the company I work for. The basic idea is that we want the package to expose a provider, which when mounted will make a request to the server that returns an array of permission strings that are then provided to the children components through context. We also want a function can which can be called within the provider which will take a string argument and return a boolean based on whether or not that string is present in the permissions array provided by context.
I was following along with this article but any time I call can from inside the provider, the context always comes back as undefined. Below is an extremely simplified version without functionality that I've been playing with to try to figure out what's going on:
useCan/src/index.js:
import React, { createContext, useContext, useEffect } from 'react';
type CanProviderProps = {children: React.ReactNode}
type Permissions = string[]
// Dummy data for fake API call
const mockPermissions: string[] = ["create", "click", "delete"]
const CanContext = createContext<Permissions | undefined>(undefined)
export const CanProvider = ({children}: CanProviderProps) => {
let permissions: Permissions | undefined
useEffect(() => {
permissions = mockPermissions
// This log displays the expected values
console.log("Mounted. Permissions: ", permissions)
}, [])
return <CanContext.Provider value={permissions}>{children}</CanContext.Provider>
}
export const can = (slug: string): boolean => {
const context = useContext(CanContext)
// This log always shows context as undefined
console.log(context)
// No functionality built to this yet. Just logging to see what's going on.
return true
}
And then the simple React app where I'm testing it out:
useCan/example/src/App.tsx:
import React from 'react'
import { CanProvider, can } from 'use-can'
const App = () => {
return (
<CanProvider>
<div>
<h1>useCan Test</h1>
{/* Again, this log always shows undefined */}
{can("post")}
</div>
</CanProvider>
)
}
export default App
Where am I going wrong here? This is my first time really using React context so I'm not sure where to pinpoint where the problem is. Any help would be appreciated. Thanks.
There are two problems with your implementation:
In your CanProvider you're reassigning the value in permissions with =. This will not trigger an update in the Provider component. I suggest using useState instead of let and =.
const [permissions, setPermissions] = React.useState<Permissions | undefined>();
useEffect(() => {
setPermissions(mockPermissions)
}, []);
This will make the Provider properly update when permissions change.
You are calling a hook from a regular function (the can function calls useContext). This violates one of the main rules of Hooks. You can learn more about it here: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions
I suggest creating a custom hook function that gives you the can function you need.
Something like this, for example
const useCan = () => {
const context = useContext(CanContext)
return () => {
console.log(context)
return true
}
}
Then you should use your brand new hook in the root level (as per the rules of hooks) of some component that's inside your provider. For example, extracting a component for the content like so:
const Content = (): React.ReactElement => {
const can = useCan();
if(can("post")) {
return <>Yes, you can</>
}
return null;
}
export default function App() {
return (
<CanProvider>
<div>
<h1>useCan Test</h1>
<Content />
</div>
</CanProvider>
)
}
You should use state to manage permissions.
Look at the example below:
export const Provider: FC = ({ children }) => {
const [permissions, setPermissions] = useState<string[]>([]);
useEffect(() => {
// You can fetch remotely
// or do your async stuff here
retrivePermissions()
.then(setPermissions)
.catch(console.error);
}, []);
return (
<CanContext.Provider value={permissions}>{children}</CanContext.Provider>
);
};
export const useCan = () => {
const permissions = useContext(CanContext);
const can = useCallback(
(slug: string) => {
return permissions.some((p) => p === slug);
},
[permissions]
);
return { can };
};
Using useState you force the component to update the values.
You may want to read more here

Destructure using rest parameters

I am trying to destructure a React context based on rest parameters passed to the hook.
Say I am passing an array of enums and want to return only the ones that are passed into the hook.
Here's my interface context type
enum ConfigItem {
SomeItem = 'SomeItem',
AnotherItem = 'AnotherItem',
}
type Config = Record<ConfigItem, boolean>;
type ConfigState = {
config: Config;
}
and the hook itself
const useConfig = (...configArgs: ConfigItem) => {
const configContext = useContext(ConfigContext);
const { config } = configContext;
const { ...configArgs } = config; // Duplicate identifier 'configArgs'.
return configArgs;
}
I would like to use it like so
const config = useConfig(ConfigItem.SomeItem, ConfigItem.AnotherItem);
and that would return an object with relevant properties. I might want to pass a single arg but potentially many.
The above const would return this (true/false is whatever would be in the context but that is not in scope of the question)
{
SomeItem: true/false,
AnotherItem: true/false,
}
but if I would only pass one of them I expect to see a single property.
By their nature, rest parameters are arrays, so you need an array type:
const useConfig = (...configArgs: ConfigItem[]) => {
// ^^−−−−−−−−−−−−−−−−−−−−−−−−−
// ...
If you pass just one argument, the array will have one element, but it will still be an array.
In a comment you've said:
My problem is how to return only the provided args properties but thanks for the correction.
If you mean you're having trouble creating the object you want to return, you can do that in a few ways; I'd probably lean toward Object.fromEntries (you'll need to polyfill if targeting pre-ES2019 environments):
const useConfig = (...configArgs: ConfigItem[]) => {
const configContext = useContext(ConfigContext);
const { config } = configContext;
return Object.fromEntries(
configArgs.map(itemKey => [itemKey, config[itemKey]])
) as Record<ConfigItem, boolean>;
};
You may need a type guard to avoid

Is separating data retrieval code into interfaces and impl. of those interfaces a good idea in React?

My question is: is the below pattern a good idea in React or no? I come from Java world where this type of code is standard. However, I've ran into several things that, while being a good idea in Java, are NOT a good idea in ReactJS. So I want to make sure that this type of code structure does not have weird memory leaks or hidden side-effects in the react world.
Some notes on below code: I'm only putting everything in the same file for brevity purposes. In real life, the react component the interface and the class would all be in their own source files.
What I'm trying to do: 1) Separate the display logic from data access logic so that my display classes are not married to a specific implementation of talking to a database. 2) Separating DAO stuff into interface + class so that I can later use a different type of database by replacing the class implementaton of the same DAO and won't need to touch much of the rest of the code.
so, A) Is this a good idea in React? B) What sort of things should I watch out for with this type of design? and C) Are there better patterns in React for this that I'm not aware of?
Thanks!
import { useState, useEffect } from 'react';
interface Dao {
getThing: (id: string) => Promise<string>
}
class DaoSpecificImpl implements Dao {
tableName: string;
constructor(tableName: string) {
this.tableName = tableName;
}
getThing = async (id: string) => {
// use a specific database like firebase to
// get data from tabled called tablename
return "herp";
}
}
const dao: Dao = new DaoSpecificImpl("thingies");
const Display: React.FC = () => {
const [thing, setThing] = useState("derp");
useEffect(() => {
dao.getThing("123").then((newThing) =>
setThing(newThing));
});
return (
<div>{thing}</div>
)
}
export default Display;
https://codesandbox.io/s/competent-taussig-g948n?file=/src/App.tsx
The DaoSpecificImpl approach works however I would change your component to use a React hook:
export const useDAO = (initialId = "123") => {
const [thing, setThing] = useState("derp");
const [id, setId] = useState(initialId);
useEffect(() => {
const fetchThing = async () => {
try{
const data = await dao.getThing(id);
setThing(data);
}catch(e){
// Handle errors...
}
}
fetchThing();
}, [id]);
return {thing, setId};
}
using the hook in your component:
const Display = () => {
const {thing, setId} = useDao("123"); // If you don't specify initialId it'll be "123"
return <button onClick={() => setId("234")}>{thing}</button> // Pressing the button will update "thing"
}
Side note: You could also use a HOC:
const withDAO = (WrappedComponent, initialId = "123") => {
.... data logic...
return (props) => <WrappedComponent {...props} thing={thing} setId={setId}/>
};
export default withDAO;
E.g. using the HOC to wrap a component:
export default withDao(Display); // If you don't specify initialId it'll be "123"

How to use useEffect correctly with useContext as a dependency

I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.

Resources