Pass component in Next.js getInitialProps to Layout - reactjs

I'm trying to have a <PageHeader /> component in a <Layout /> which is used on each page. The goal is to use getInitialProps in each page to set the title, subtitle, and actions. The actions are optional and function as page-specific actions such as saving a post.
The layout looks kind of like this:
|----------------------|----------|
| title | actions? |
| subtitle | |
|---------------------------------|
As stated, the <PageHeader /> component is used inside of the <Layout /> component, and I am able to pass basic props using getInitialProps inside of a given page, but passing a component throws an error Error: Objects are not valid as a React child
const Actions = () => {} // menu of button CTA's
const MyPage = () => {} // Page
const MyPage.getInitialProps = (ctx) => {
return {
title: 'Title',
subtitle: 'subtitle',
actions: <Actions />
}
}
const Layout = ({ pageProps, children }) => {
return (
<div>
<PageHeader {...pageProps} />
{children}
</div>
)
}
const PageHeader = ({ actions, title, subtitle }) => {
return (
<div>
<h3>{title}</h3>
<span>{subtitle}</span>
<div>{actions}</div>
</div>
)
}
It's unclear to me if this is a bad pattern or a dead-end. If Next.js's getInitialProps can't or shouldn't be used this way, is there a better way to achieve a dynamic, page-specific component inside of a layout component?

Related

rendering a component inside a react page using Menu

I have a menu and the main body of the page. I have created different pages as components with some text. All I want is that when I click on the side bar menu, the components are displayed in the main page. How can I make this work?
const items2 = [{
label: 'Rice',
key: 'rice',
},
{
label: 'AB Test',
key: 'ab',
}]
const MainLayout = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
const navigate = useNavigate();
const onClick = (e)=>{navigate(`/${e.key}`)}
return (
<Layout>
<Sider >
<Menu
mode="inline"
items={items2}
onClick = {onClick}
/>
</Sider>
<Content >
//I Want to open the pages here
</Content>
</Layout>
</Content>
To render a component inside other component, React provides a special props name children.
To achieve your requirement, you can do like this:
MainLayout.js:
export const MainLayout = ({children}) => {
const {
token: { colorBgContainer },
} = theme.useToken();
const navigate = useNavigate();
const onClick = (e)=>{navigate(`/${e.key}`)}
return (
<Layout>
<Sider>
<Menu
mode="inline"
items={items2}
onClick={onClick}
/>
</Sider>
<Content>
{children}
</Content>
</Layout>
)
}
In MainLayout.js, you only need to write {children} inside component <Content>, React will do the rest for you to pass the content of Rice or AB or whatever for you. In each page, just place <MainLayout> at the top of the outer of rest of your code.
Please see 2 example files below.
Rice.js:
import MainLayout from './MainLayout';
export const Rice = () => {
// Do some stuffs here...
return (
<MainLayout>
<div>
<h2>Best rated rice in the World</h2>
<ol>
<li>Pinipig</li>
<li>Riz de Camargue</li>
...
</ol>
<div>
</MainLayout>
)
}
Corn.js:
import MainLayout from './MainLayout';
export const Corn = () => {
// Do some stuffs here...
return (
<MainLayout>
<div>
<h2>Best Corn Side Dish Recipes</h2>
<ol>
<li>Whipped-Cream Corn Salad</li>
<li>Classic Grilled Corn on the Cob</li>
...
</ol>
<div>
</MainLayout>
)
}
You can read more and play around with the example code from React's official documents.
It is the basic concept of React, so before you start to build something big, I suggest to follow this docs first or find some series of React tutorials for beginner, they will explain key concepts of React so you would not save more time.
You need to use react-router-dom to navigate when you click other MenuItem. Create your own RouterProvider and put it in the Content.
<Content>
<RouterProvider router={router}>
</Content>
EDIT
Now you have two way to navigate to your Component. First is using Link and set it to your label.
{
label: <Link title="Rice" to="/rice">Rice</Link>,
key: 'rice',
}
Second way is hook useNavigate
const navigate = useNavigate();
const onClick = (e)=>{navigate(`/${e.key}`)}
//Add to Menu
<Menu
onClick={onClick}
//other props
/>

React - easy solution for global state or antipattern?

In my React app I have main component that contians the whole state of the application and also functions used to modify the state.
In order to get access to the state and the functions in all subcomponents I'm passing this from the main component through subcomponent attributes (app):
class App extends React.Component {
state = DefaultState;
funcs = getController(this);
render() {
return (
<Header app={this} />
<Page app={this} />
<Footer app={this} />
)
}
}
function getController(app: App) {
return {
switchPage: (page: string) => {
app.setState({ page: page });
},
}
}
Therefore in subcomponents I can access the state variables and modify them like this:
const HeaderComponent: React.FC<CompProps> = (props) => {
return (
<div>
<h1>{props.app.state.currentPage}</h1>
<button onClick={(e) => {props.app.funcs.switchPage('home')}}>Home</button>
</div>
)
}
This is the quickest/simplest solution I've found for global state. However I've never seen this in any tutorial or so. I guess main problem is the whole app will rerender when a single value is changed in global state, but the same goes for React Context API.
Question is that are there any disadvantages of this approach or a reason to not use this?
You can save func to an exported let variable and utilize the most recent version of func without re-rendering, but as this isn't a common occurrence, you won't find much information about it. Since it's simply javascript, any known hack will work. Also, the part of your question regarding react-context re-rendering is correct although you must consider that it will re-render and it will be more pruned for optimization of unmodified siblings.
You may alternatively supply a simple ref (useRef) to those components, which will allow them to access the most recent version of func, but because the ref reference itself does not change when the page is re-rendered, they will not be updated for function change.
I'm using react functional component but the class base may be so similar
export let funcs = null
const App = () => {
funcs = getController();
render() {
return (
<Header />
<Page />
<Footer />
)
}
}
// header component
import { funcs as appFuncs } from '~/app.js'
const HeaderComponent: React.FC<CompProps> = (props) => {
return (
<div>
{/* same thing can be happened for the state */}
<button onClick={(e) => {appFuncs.switchPage('home')}}>Home</button>
</div>
)
}
Hooks version
const App = () => {
const ref = useRef();
funcs = getController();
ref.current = {state, funcs};
// note ref.current changes not the ref itself
render() {
return (
<Header app={ref} />
<Page app={ref} />
<Footer app={ref} />
)
}
}
// header
const HeaderComponent: React.FC<CompProps> = (props) => {
return (
<div>
<h1>{props.app.current.state.currentPage}</h1>
<button onClick={(e) => {props.app.current.func.switchPage('home')}}>Home</button>
</div>
)
}
Any suggestions or other techniques will be much appreciated.

When using a functional component, why do parent state changes cause child to re-render even if context value doesn't change

I'm switching a project from class components over to functional components and hit an issue when using a context.
I have a parent layout that contains a nav menu that opens and closes (via state change in parent). This layout component also contains a user property which I pass via context.
Everything worked great before switching to the functional components and hooks.
The problem I am seeing now is that when nav menu is opened from the layout component (parent), it is causing the child component to re-render. I would expect this behavior if the context changed, but it hasnt. This is also only happening when adding useContext into the child.
I'm exporting the children with memo and also tried to wrap a container with the children with memo but it did not help (actually it seemed to cause even more renders).
Here is an outline of the code:
AppContext.tsx
export interface IAppContext {
user?: IUser;
}
export const AppContext = React.createContext<IAppContext>({});
routes.tsx
...
export const routes = <Layout>
<Switch>
<Route exact path='/' component={Home} />
<Route exact path='/metrics' component={Metrics} />
<Redirect to="/" />
</Switch>
</Layout>;
Layout.tsx
...
const NavItems: any[] = [
{ route: "/metrics", name: "Metrics" }
];
export function Layout({ children }) {
const aborter = new AbortController();
const history = useHistory();
const [user, setUser] = React.useState<IUser>(null);
const [navOpen, setNavOpen] = React.useState<boolean>(false);
const [locationPath, setLocationPath] = React.useState<string>(location.pathname);
const contextValue = {
user
};
const closeNav = () => {
if (navOpen)
setNavOpen(false);
};
const cycleNav = () => {
setNavOpen(prev => !prev);
};
React.useEffect(() => {
Fetch.get("/api/GetUser", "json", aborter.signal)
.then((user) => !aborter.signal.aborted && !!user && setUser(user))
.catch(err => err.name !== 'AbortError' && console.error('Error: ', err));
return () => {
aborter.abort();
}
}, []);
React.useEffect(() => {
return history.listen((location) => {
if (location.pathname != locationPath)
setLocationPath(location.pathname);
})
}, [history]);
const navLinks = NavItems.map((nav, i) => <li key={i}><Link to={nav.route} onClick={closeNav}>{nav.name}</Link></li>);
return (
<div className="main-wrapper layout-grid">
<header>
<div className="header-bar">
<div className="header-content">
<div className="mobile-links-wrapper">
<ul>
<li>
<div className="mobile-nav-bars" onClick={cycleNav}>
<Icon iconName="GlobalNavButton" />
</div>
</li>
</ul>
</div>
</div>
</div>
<Collapse className="mobile-nav" isOpen={navOpen}>
<ul>
{navLinks}
</ul>
</Collapse>
</header>
<AppContext.Provider value={contextValue} >
<main role="main">
{children}
</main>
</AppContext.Provider>
<a target="_blank" id="hidden-download" style={{ display: "none" }}></a>
</div>
);
}
Metrics.tsx
...
function Metrics() {
//Adding this causes re-renders, regardless if I use it
const { user } = React.useContext(AppContext);
...
}
export default React.memo(Metrics);
Is there something I am missing? How can I get the metrics component to stop rendering when the navOpen state in the layout component changes?
Ive tried memo with the switch in the router and around the block. I've also tried moving the contextprovider with no luck.
Every time your Layout component renders, it creates a new object for the contextValue:
const contextValue = {
user
};
Since the Layout component re-renders when you change the navigation state, this causes the context value to change to the newly created object and triggers any components depending on that context to re-render.
To resolve this, you could memoize the contextValue based on the user changing via a useMemo hook and that should eliminate the rendering in Metrics when the nav state changes:
const contextValue = React.useMemo(() => ({
user
}), [user]);
Alternatively, if you don't really need the object, you could simply pass the user as the context value directly:
<AppContext.Provider value={user}>
And then access it like:
const user = React.useContext(AppContext);
That should accomplish the same thing from an unnecessary re-rendering point of view without the need for useMemo.

Is there a way to bubble up a ref for a nested component rendered by a function?

I'm using React JS and Stripe JS to render a reusable payment form.
This is working fine except for one case which requires a button to be rendered outside of the component.
I was hoping to use a ref for the custom payment form but, because of the way Stripe requires the component to be injected and wrapped with an ElementsConsumer component, the underlying component class that is rendered isn't accessible so the 'ref' property cannot be used.
In the below example, would there be a way for me to allow a ref to be used for the '_PaymentForm' class when the 'PaymentForm' function is used to render it?
Thanks!
class _PaymentForm extends React.Component<PaymentFormProps, PaymentFormState> {
...
}
const InjectedPaymentForm = (props: PaymentFormProps) => {
return (
<ElementsConsumer>
{({ elements, stripe }) => (
<_PaymentForm {...props} elements={elements} stripe={stripe} />
)}
</ElementsConsumer>
);
}
export const PaymentForm = (props: PaymentFormProps) => {
const stripePromise = loadStripe(props.stripeApiKey);
return (
<Elements stripe={stripePromise}>
<InjectedPaymentForm {...props} />
</Elements>
);
}
export default PaymentForm;

Redux/React - Why I can't bind state to this.props on component which is in iframe?

I have a specific scenario in my react/redux/express universal project (server-side rendering).
(1)First I defined my routes like so: [ routes.jsx ]
export default (
<Route component={App} path="/">
<Route component={MainView} path="main">
<IndexRoute component={ThemeIndex}></IndexRoute>
</Route>
<Route component={AnotherView} path="preview" />
</Route>
);
As you see, when url route is: localhost:3000/preview, react-router will use AnotherView component.
(2)Now focus on ThemeIndex component: [ ThemeIndex.jsx ]
export default class ThemeIndex extends Component {
render() {
return (
<div>
<h2>Index</h2>
<Frame />
<Control />
</div>
);
}
}
(3)Frame component like so: [ Frame.jsx ]
class Frame extends Component {
render() {
const { text, uid } = this.props.infos;
const themeUrl = `http://localhost:3000/preview?id=${uid}`;
//console.log('Frame theme:', text);
//console.log('Frame uid:', uid);
return (
<div className="col-md-8 panel panel-default">
<div className="embed-responsive embed-responsive-16by9 panel-body">
<iframe src={themeUrl}></iframe>
</div>
<div className="details" >
{text}
</div>
</div>
);
}
}
export default connect(
(state) => {
return {
infos: state.infos
}
}
)(Frame);
Here I use iframe tag, its src is http://localhost:3000/preview?id=xxxx, so it means it will link AnotherView component to be iframe's page.
(4)AnotherView Component like so:
class AnotherView extends Component {
render() {
const { text, uid } = this.props.infos;
//console.log('AnotherView theme:', text);
//console.log('AnotherView uid:', uid);
return (
<div>
<div >Another View</div>
<div>
{text}
</div>
<div>{uid}</div>
</div>
);
}
}
export default connect(
(state) => {
console.log('another view trigger state:', state);
return {
infos: state.infos
}
}
)(AnotherView);
(4)And I have Control component for making dynamic value: [ Component.jsx ]
class Control extends Component {
render(){
var uid = () => Math.random().toString(34).slice(2);
return (
<input
onChange={(event) => this.props.addTodo({text:event.target.value, uid:uid()})
/>
)
}
}
export default connect(
(state) => {
return {
infos: state.infos
}
}
)(Control);
(5)List extra files, Action and Reducer:
[ action.js ]
export function addTodo (attrs) {
return {
type: 'ADD_TODO',
attrs
};
}
[ reducer.js ]
export default (state = {text:'', uid:''}, action) => {
switch(action.type) {
case 'ADD_TODO':
return Object.assign({}, state, action.attrs);
default:
return state;
}
}
Here is Store configuration on server.js:
app.use( (req, res) => {
console.log('server - reducers:', reducers);
const location = createLocation(req.url);
const reducer = combineReducers({infos: infosReducer});
const store = applyMiddleware(promiseMiddleware)(createStore)(reducer);
match({ routes, location }, (err, redirectLocation, renderProps) => {
.......
function renderView() {
const createElement = (Component, props) => (
<Component
{...props}
radiumConfig={{ userAgent: req.headers['user-agent'] }}
/>
);
const InitialView = (
<Provider store={store}>
<RoutingContext
{...renderProps}
createElement={createElement} />
</Provider>
);
const componentHTML = renderToString(InitialView);
const initialState = store.getState();
......
my application state is like :
{
infos:{
text: '',
uid: ''
}
}
(6)Now I key some words on input in Control component. When the input onChange will trigger addTodo action function to dispatch action in reducer, finally change the state. In common, the state changing will effect Frame component and AnotherView component, because I used react-redux connect, bind the state property to this.props on the component.
But in fact, there is a problem in AnotherView component. in Frame component, console.log value display the text you key in input correctly. In AnotherView component, even the connect callback will be trigger (console.log will print 'another view trigger state: ...') , the console.log in render is undefined, like:
console.log('AnotherView theme:', text); //return AnotherView theme: undefined
console.log('AnotherView uid:', uid); //return AnotherView uid: undefined
I found the main reason: AnotherView component is in iframe. Because if I remove iframe, put AnotherView component directly here, like so:
return (
<div className="col-md-8 panel panel-default">
<div className="embed-responsive embed-responsive-16by9 panel-body">
<AnotherView/>
</div>
<div className="details" >
{text}
</div>
</div>
);
then I can bind state properties on this.props in AnotherView component successfully, then insert {text} on JSX html, you can see the value changing real time when you key input value on Control component. if I use iframe to link AnotherView component be its page, you can't see any changing {text} value, because my text default value is empty string value.
How do I bind state properties to this.props in the component which is in iframe when state changing?
Update
I can't get the latest state in iframe (source is React component), when I changing state in another component, and actually the mapStateToProps was triggered!(means iframe source component) but its state is not the latest in mapStateToProps function. it does not really concerns with the react-redux library?
This is the latest state should be in component:
Below is iframe source component, it can't get the latest state:
If you load an app in an iframe from a script tag, it will load a separate instance of the app. This is the point of iframes: they isolate code.
Two separate instances of the app won’t “see” updates from each other. It’s like if you open the app in two separate browser tabs. Unless you add some method of communication between them, they will not share state.
It is not clear why you want to render a frame rather than a component directly. But if you really need frames, a simple option would be to use to render the component into the frame rather than load a separate app there. You can use a library like react-frame-component for this. Or, simpler, you can just not use frames at all, as it is not clear what purpose they serve. Usually people want them to isolate an app instance but this seems contrary to what you seem to want.

Resources