in my react native main file (App.js) I have a handler for firebase notifications, in the foregroundNotifications listener I want to check current route name but problem is I don't have access to navigation prop, that's why I created a ref to navigation in another file which I then import in App.js.
In my RootNavigation.js
import * as React from 'react';
export const isReadyRef = React.createRef();
export const navigationRef = React.createRef();
export function navigate(name, params) {
if (isReadyRef.current && navigationRef.current) {
// Perform navigation if the app has mounted
navigationRef.current.navigate(name, params);
} else {
// You can decide what to do if the app hasn't mounted
// You can ignore this, or add these actions to a queue you can call later
}
}
This allowed me to use navigate (added the method following another stackoverflow post) , but how can I get current route name from NavigationRoot.js?
How I use it in my App.js (I added navigate, but how can I add getCurrentRoute?)
import { navigationRef, isReadyRef } from '#env/RootNavigation.js';
RootNavigation.navigate('NewScreen');
<NavigationContainer ref={navigationRef} onReady={ () => { isReadyRef.current = true; } }>
<ROOTSTACK1></ROOTSTACK1>
</NavigationContainer>
Your RootNavigation file should be like in this Example.
add this below function in RootNavigation file
function getCurrentRoute(){
if (navigationRef.isReady()) {
const route=navigationRef.getCurrentRoute();
console.log(route);
// sample output {key:"Home-k2PN5aWMZSKq-6TnLUQNE",name:"Home"}
return route.name;
}
}
And wherever you want to use this function
// any js module
import * as RootNavigation from './path/to/RootNavigation.js';
// ...
RootNavigation.getCurrentRoute();
Related
I have a component that I need to get the current full URL in, here's the simplied version:
/**
* Share dropdown component
*/
export const ShareDropdown: React.FC<{ className: string }> = ({
className,
}) => {
return (
<div className={className}>{currentURL}</div>
)
}
Did some Googling and saw some say use Next's router (but didn't see that return the domain), other using getInitialProps but didn't see how to incorporate into a functional component.
New to all of this, so just trying to find the best way, seems like it should be simple.
You can use simply useRouter from nextjs just like this
import { useRouter } from 'next/router';
...
const { asPath } = useRouter();
...
return (
<div className={className}>http://example.com{asPath}</div>
)
Additionally you can save http://example.com in your environment variables
If you have configured a base path you need to get it
import { useRouter, basePath } from 'next/router';
More info about useRouter here
Store window.location.href as a variable. so, like: const URL = window.location.href and then simply put {URL} here:
/**
* Share dropdown component
*/
export const ShareDropdown: React.FC<{ className: string }> = ({
className,
}) => {
const URL = window.location.href
return (
<div className={className}>{URL}</div>
)
}
I have a very simple class based component. Which looks like the following:
class MyComponent extends React.Component {
onPressButton () {
console.warn('button pressed')
const { contextFunction } = this.context
contextFunction()
}
render () {
return (
<div>
My Component
<button onClick={() => onPressButton()}>Press button</button>
</div>
)
}
}
MyComponent.contextType = SomeContext
That is all fine and well and works as expected. However, I am having trouble adding unit tests with jest and enzyme. My current code looks as follows:
test('should render test Press button', async () => {
const contextFunctionMock = jest.fn()
const wrapper = shallow(<MyComponent {...props} />)
wrapper.instance().context = { contextFunction: contextFunctionMock }
console.log('wrapper.instance()', wrapper.instance())
await wrapper.instance().onPressButton() // this works just fine
expect(contextFunctionMock).toHaveBeenCalled() // this errors, basically because ti complains contextFunction is not a function
})
As you can see above, I console.logged my wrapper.instance() to see what is going on.
Interestingly enough, the context on the root of the instance object is indeed what I expected it to be based on setting the context, which is something like:
context: {
contextFunction: [Function: mockConstructor] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
[...Other mock function properties]
}
...
However, there is a second context, which is in the updater property of the wrapper.instance(), and it is an empty object. Basically looks like the following:
updater: <ref *2> Updater {
_renderer: ReactShallowRenderer {
_context: {},
...
}
Not exactly sure if this is the context being used for my component's unit test, but it is currently just an empty object, which makes me think this may be the one being used for it.
Anyway, how can I properly mock my context functions to run on this particular unit tests? Also, why is this happening but does not happen in others with a similar set of circumstances?
Problem
A fundamental problem with your code above is that there's no way to assert that the context function is successfully/failing to be called. Right now, you're clicking a button, but there isn't any indication on what's happening after the button is clicked (nothing is being changed/updated within the context/component to reflect any sort of UI change). So asserting that a contextual function is called won't be beneficial if there's no result of clicking the button.
In addition to the above, the enzyme-adapter doesn't support context that uses the createContext method.
However, there's a work-around for this limitation! Instead of unit testing the component, you'll want to create an integration test with the context. Instead of asserting that a contextual function was called, you'll make assertions against the result of clicking on the button that changes context and how it affects the component.
Solution
Since the component is tied to what's in context, you'll create an integration test. For example, you'll wrap the component with context in your test and make assertions against the result:
import * as React from "react";
import { mount } from "enzyme";
import Component from "./path/to/Component";
import ContextProvider from "./path/to/ContextProvider";
const wrapper = mount(
<ContextProvider>
<Component />
</ContextProvider>
);
it("updates the UI when the button is clicked", () => {
wrapper.find("button").simulate("click");
expect(wrapper.find(...)).toEqual(...);
})
By doing the above, you can make assertions against contextual updates within the Component. In addition, by using mount, you won't have to dive into the ContextProvider to view the Component markup.
Demo Example
This demo utilizes context to toggle a theme from "light" to "dark" and vice versa. Click on the Tests tab to run the App.test.js integration test.
Code Example
App.js
import * as React from "react";
import { ThemeContext } from "./ThemeProvider";
import "./styles.css";
class App extends React.PureComponent {
render() {
const { theme, toggleTheme } = this.context;
return (
<div className="app">
<h1>Current Theme</h1>
<h2 data-testid="theme" className={`${theme}-text`}>
{theme}
</h2>
<button
className={`${theme}-button button`}
data-testid="change-theme-button"
type="button"
onClick={toggleTheme}
>
Change Theme
</button>
</div>
);
}
}
App.contextType = ThemeContext;
export default App;
ThemeProvider.js
import * as React from "react";
export const ThemeContext = React.createContext();
class ThemeProvider extends React.Component {
state = {
theme: "light"
};
toggleTheme = () => {
this.setState((prevState) => ({
theme: prevState.theme === "light" ? "dark" : "light"
}));
};
render = () => (
<ThemeContext.Provider
value={{ theme: this.state.theme, toggleTheme: this.toggleTheme }}
>
{this.props.children}
</ThemeContext.Provider>
);
}
export default ThemeProvider;
index.js
import * as React from "react";
import ReactDOM from "react-dom";
import ThemeProvider from "./ThemeProvider";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);
Test Example
An example of how to test against the demo example above.
withTheme.js (an optional reusable testing factory function to wrap a component with context -- especially useful for when you may want to call wrapper.setProps() on the root to update a component's props)
import * as React from "react";
import { mount } from "enzyme";
import ThemeProvider from "./ThemeProvider";
/**
* Factory function to create a mounted wrapper with context for a React component
*
* #param Component - Component to be mounted
* #param options - Optional options for enzyme's mount function.
* #function createElement - Creates a wrapper around passed in component with incoming props (now we can use wrapper.setProps on root)
* #returns ReactWrapper - a mounted React component with context.
*/
export const withTheme = (Component, options = {}) =>
mount(
React.createElement((props) => (
<ThemeProvider>{React.cloneElement(Component, props)}</ThemeProvider>
)),
options
);
export default withTheme;
App.test.js
import * as React from "react";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import withTheme from "./withTheme";
import App from "./App";
configure({ adapter: new Adapter() });
// wrapping "App" with some context
const wrapper = withTheme(<App />);
/*
THIS "findByTestId" FUNCTION IS OPTIONAL!
I'm using "data-testid" attributes, since they're static properties in
the DOM that are easier to find within a "wrapper".
This is 100% optional, but easier to use when a "className" may be
dynamic -- such as when using css modules that create dynamic class names.
*/
const findByTestId = (id) => wrapper.find(`[data-testid='${id}']`);
describe("App", () => {
it("initially displays a light theme", () => {
expect(findByTestId("theme").text()).toEqual("light");
expect(findByTestId("theme").prop("className")).toEqual("light-text");
expect(findByTestId("change-theme-button").prop("className")).toContain(
"light-button"
);
});
it("clicking on the 'Change Theme' button toggles the theme between 'light' and 'dark'", () => {
// change theme to "dark"
findByTestId("change-theme-button").simulate("click");
expect(findByTestId("theme").text()).toEqual("dark");
expect(findByTestId("theme").prop("className")).toEqual("dark-text");
expect(findByTestId("change-theme-button").prop("className")).toContain(
"dark-button"
);
// change theme to "light"
findByTestId("change-theme-button").simulate("click");
expect(findByTestId("theme").text()).toEqual("light");
});
});
As for today, the new context API is not supported by enzyme, the only solution I found is to use this utility https://www.npmjs.com/package/shallow-with-context
import { configure, shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { withContext } from "shallow-with-context";
import MyComponent from "./MyComponent";
configure({ adapter: new Adapter() });
describe("Context", () => {
it("should render test Press button", async () => {
const contextFunctionMock = jest.fn();
const context = { contextFunction: contextFunctionMock };
const MyComponentWithContext = withContext(MyComponent, context);
const wrapper = shallow(<MyComponentWithContext />, { context });
await wrapper.instance().onPressButton();
expect(contextFunctionMock).toHaveBeenCalled();
});
});
https://codesandbox.io/s/enzyme-context-test-xhfj3?file=/src/MyComponent.test.tsx
This is a Lazy Route component I wrote a while ago (code below).
This is what it does:
It initially render a lazyLoading = true state to show some spinner or something.
Then on useEffect it will dynamic import() a component module and set the LazyComponent state. Like setLazyComponent(module)
Then it will turn the loading to false and render <LazyComponent.default />
Note: It works as intended.
My question is:
I've tried to do the following:
Set the default property to the state. As in setLazyComponent(module.default). Since SomeComponent, which is the component that is being lazy loaded has a single default export.
Then I should be able to render just <LazyComponent/> instead of <LazyComponent.default/>
But it does not work.
And I get this error:
Why does it not work? All the rest of the code is the same. The only change I'm trying to make is the place where I access the default property.
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
const LS = {};
LS.Container_DIV = styled.div`
`;
async function lazyRender() {
const module = await import("./SomeComponent");
return new Promise((resolve) => {
resolve(module);
});
}
function LazyRoute(props) {
console.log('Rendering LazyRoute...');
const [lazyLoading,setLazyLoading] = useState(true);
const [LazyComponent,setLazyComponent] = useState(null);
useEffect(() => {
async function getLazyComponent() {
const module = await lazyRender();
setLazyComponent(module);
setLazyLoading(false);
}
getLazyComponent();
},[]);
return(
lazyLoading ?
<div>I am Lazy Loading....</div>
: <LazyComponent.default {...props}/>
);
}
export default React.memo(LazyRoute);
I'm trying to access functions inside a class based component which can be used throughout the project. The reason I'm thinking class based is because these request/ functions require an init() method to be called before accessing such data every time. For example:
SharedSDKFile .js
import Facebook from 'facebook-sdk';
class SharedSDKFile extends Component {
constructor() {
Facebook.init({// init some stuff})
}
async user() {
return Facebook.getUser()
}
render() {
return(// ????????????????)
}
}
// *****************************************************
Dashboard.js
// *****************************************************
import Facebook from '../{path}/SharedSDKFile'
const dashboard = () => {
let [person,setPerson] = useState()
// cool function to get and set Users
let user = getUser();
setPerson(user)
// End of cool function
}
I even tried structuring it with a different approach just exporting functions
SharedSDKFile.js
async init() {
// init stuff
}
export const getUser = async(data) => {
init()
// get user
}
// *****************************************************
Dashboard.js
// *****************************************************
import {getUser}from '../{path}/SharedSDKFile'
const dashboard = () => {
let [person,setPerson] = useState()
// cool function to get and set Users
let user = getUser();
setPerson(user)
// End of cool function
}
While a file that exports your function works, the state disappears on reload/ refresh.
Perhaps there is a better solution to this and I'm overthinking it. I have considered redux or localstate, but I will have several functions inside the sharedSDKFile.js which will require several action and reducers...
I am trying to prevent invoking multiple init() and redundancy if I am to import, for example, the FacebookSDK in every file that needs it.
I like using a context singleton approach to isolate external services that can be used app-wide, which may or may not suit what you want to do. It uses context providers/consumers rather than things like Redux.
This isn't the complete picture but hopefully it provides some idea of how the approach might work for you:
FacebookContext.js (or AWSContext.js, or... any specific service)
import React, { Component, createContext } from "react";
import Facebook from "facebook-sdk";
export const FacebookContext = createContext({});
class FacebookProvider extends Component {
state = {
user: null,
// whatever else you need to expose to calling components, like
// lastLoggedIn: null,
// verified: false,
// ...
};
componentDidMount() {
Facebook.init({
// init some stuff
})
}
setUser = user => {
this.setState({
user
});
}
// whatever other methods/data you want this class to expose
render() {
return (
<FacebookContext.Provider
value={{
user: this.state.user // available with FacebookContext.user
// other state values
//
setUser: this.setUser // available with FacebookContext.setUser
// other class methods
}}
>
{this.props.children}
</FacebookContext.Provider>
);
}
}
export default FacebookProvider;
Add the Provider to your top-line app file, something like this in e.g. App.js:
import FacebookProvider from "/path/to/FacebookContext";
// ...
class App extends React.Component {
render() {
return (
<FacebookProvider>
{yourAppRenderStuff}
</FacebookProvider>
);
}
}
Add the Consumer to any calling components:
import { FacebookContext } from "/path/to/FacebookContext";
class SomethingComponent extends Component {
componentDidMount() {
const { facebookContext } = this.props;
facebookContext.setUser(`some user`);// available in other components
console.log(facebookContext.user);// broadcasted to other components
}
render() {
return (
<></>
);
}
}
const Something = () => (
<FacebookContext.Consumer>
{facebookContext => (
<SomethingComponent facebookContext={facebookContext} />
)}
</FacebookContext.Consumer>
);
export default Something;
I have a function that take a component as argument, and return another, enhanced component:
import React from 'react';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Layout, DarkBar } from 'SharedComponents/Layouts';
const myCreationFunction = ({
component,
}) => {
const Route = (props) => {
// Some code here
return (
<Layout>
<div><Link to={props.path}>LinkTitleHere</Link></div>
{React.createElement(component, {
...props,
...someOtherPropsHere,
})}
</Layout>
);
}; // The error points here
const mapStateToProps = () => ({});
const enhance = compose(
connect(mapStateToProps, someActionsHere),
)(Route);
return enhance;
};
I invoke that function in this way:
import MyComponent from './MyComponent';
import myCreationFunction from './HOC/myCreationFunction';
const Route = myCreationFunction({
component: MyComponent,
});
When I run it in the development mode, it runs smoothly. But when trying to build the app using npm run build and going through webpack, I get:
Module parse failed: Unexpected token (35:47)
You may need an appropriate loader to handle this file type.
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
> var createListRoute = function myCreationFunction((_temp = _ref, _ref2 = <_Layouts.DarkBar>
| <_Breadcrumbs2.default />
| <_Layouts.RoundAddButton path={addPath} color="white" />
What am I doing wrong?
Edit #1
It seems that the <Link> is causing the problem. Removing it fixed the problem. Also, when trying to replace it with a tag, I get the same issue. Weird.
Edit #2
I have not resolved this issue because of lack of time. I was trying for 1 hour without any progress and decided to go with button tag and onClick method that uses history to push the new url.
It was and is really weird to me, that a Link or <a> tag can break something during the build process itself. I will definitely jump deeper into it in some free time.