WP Gutenberg, how to pass variables from backend (the save function) to frontend (a React app rendered in the save function output)? - reactjs

If I have a Gutenberg block for which I gather a string that the user enters, but I want to use that string within a react app rendered in the frontend, how can I pass that string?
Defining a Gutenberg block
save: ({ attributes }) => {
window.thisVariableWillNotBeSeen = attributes
console.log(window) // here `thisVariableWillNotBeSeen` is seen, in the frontend it is not
return (
<div id="test_react"></div>
)
},
Then, a script enqueued as such (within a plugin)
add_action('wp_enqueue_scripts', 'react_enqueue');
function react_enqueue()
{
$asset_file = include(plugin_dir_path(__FILE__) . 'build/test.asset.php');
wp_enqueue_script(
'myBlock',
plugins_url('build/test.js', __FILE__),
$asset_file['dependencies'],
$asset_file['version'],
true
);
}
And scr/test.js
const { render } = wp.element
import { Test} from './components/test'
render(<Test />, document.getElementById(`test_react`))
Within export const Test, if I see there console.log(window) I cannot see the global variable I have added in the save function of before
How could I do this?

As said here https://stackoverflow.com/a/44663473/826815
it can be done by rendering a script or also a dataset property, and later fetch this data through the window object or through the DOM
save: ({ attributes }) => {
return (
<Fragment>
<div id="test_react"></div>
<div id="test_react_data" data-test={JSON.stringify(attributes)}></div>
<script type="text/javascript">{`var test_react= ${JSON.stringify(attributes)};`}</script>
</Fragment>
)
},

Related

React createProtal called outsite a JSX component not updating the DOM

I am trying to render a dynamically generated react component in a react app using createProtal.
When I call createProtal from a class the component is not rendered.
Handler.ts the class the contains the business logic
export class Handler {
private element: HTMLElement | null;
constructor(selector: string) {
this.element = document.getElementById(selector);
}
attachedEvent() {
this.element?.addEventListener("mouseenter", () => {
let cancel = setTimeout(() => {
if (this.element != null)
this.attachUi(this.element)
}, 1000)
this.element?.addEventListener('mouseleave', () => {
clearTimeout(cancel)
})
})
}
attachUi(domNode: HTMLElement) {
createPortal(createElement(
'h1',
{className: 'greeting'},
'Hello'
), domNode);
}
}
Main.tsx the react component that uses Handler.ts
const handler = new Handler("test_comp");
export default function Main() {
useEffect(() => {
// #ts-ignore
handler.useAddEventListeners();
});
return (
<>
<div id="test_comp">
<p>Detect Mouse</p>
</div>
</>
)
}
However when I repleace attachUi function with the function below it works
attachUi(domNode: HTMLElement) {
const root = createRoot(domNode);
root.render(createElement(
'h1',
{className: 'greeting'},
'Hello'
));
}
What am I missing?
React uses something called Virtual DOM. Only components that are included in that VDOM are displayed to the screen. A component returns something that React understands and includes to the VDOM.
createPortal(...) returns exactly the same as <SomeComponent ... />
So if you just do: const something = <SomeComponent /> and you don't use that variable anywhere, you can not display it. The same is with createPortal. const something = createPortal(...). Just use that variable somewhere if you want to display it. Add it to VDOM, let some of your components return it.
Your structure is
App
-children
-grand children
-children2
And your portal is somewhere else, that is not attached to that VDOM. You have to include it there, if you want to be displayed.
In your next example using root.render you create new VDOM. It is separated from your main one. This is why it is displayed

JSX fragment content is not rendering

[postslug].js
import {PostData} from '../../data/postdata'
export async function getStaticProps({ params }) {
const posts = PostData.find((p) => p.slug === params.postslug);
return {
props: {
post: posts,
},
};
}
export async function getStaticPaths() {
const paths = PostData.map((post) => ({
params: { postslug: post.slug },
}));
return { paths, fallback: false };
}
const Post = ({post}) => {
// const router = useRouter();
// const slug = router.query.postslug;
// const post = PostData.find((post1) => post1.slug === slug);
return (
<>
{post.content}
</>
)
}
PostData.js
export const PostData = [
{
id: 1,
slug: "article-blog",
content: `<><main>
<div className="postpage">
<section className="article">
<article>
<h2>Google Chrome</h2>
<p>Google Chrome is a web browser developed by Google, released in 2008. Chrome is the world's most popular web browser today!</p>
</article>
<article>
<h2>Mozilla Firefox</h2>
<p>Mozilla Firefox is an open-source web browser developed by Mozilla. Firefox has been the second most popular web browser since January, 2018.</p>
</article>
<article>
<h2>Microsoft Edge</h2>
<p>Microsoft Edge is a web browser developed by Microsoft, released in 2015. Microsoft Edge replaced Internet Explorer.</p>
</article>
</section>
</div>
</main></>`
},
]
The code written in JSX Fragments is not rendering after fetching it. It is just displayed as it is written in an array of objects. Postdata.js file is containing an array of objects. I am trying to fetch the data of blog articles using getStaticProps and getStaticPaths.
Output Like:
The first solution can be using dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{__html: post.content}} />
But as for the security problem, it will lead you to cross-site scripting (XSS) attack. So I'd propose you use html-react-parser that will help you to render a string as JSX safely.
import parse from 'html-react-parser';
const Post = ({post}) => {
return (
<>
{parse(post.content)}
</>
)
}
You have to use dangerouslySetInnerHTML in React, it is equivalent to seting innerHTML in vanilla JS refer this React.js: Set innerHTML vs dangerouslySetInnerHTML

Passing props via route Sveltekit

I am using Svelte+Sveltekit without any routing libraries.
What I would like to do is pass an object to a route, from another page via an <a> tag (or otherwise).
On one page I have a list of objects, for each object I render an item:
// home.svelte
<-- start of page -->
{#each users as user}
<a href="users/{user.username}" sveltekit:prefetch/>
{/each}
<-- end of page -->
The user object above has a few key-value pairs I want to render in the /users/{username} - which is created as a slug route:
// routes/users/[slug].svelte
<script context="module">
export async function load(ctx) {
let data = ctx.page.params;
// I'd like to be able to pass the whole user object from the <a> tag in home.svelte, and access it from ctx.page.params if possible
return { props: { slug: data.slug, user: data.user } }
}
</script>
<script>
export let slug;
export let user;
</script>
<div>
<h1>{slug}</h1>
<h1>{JSON.stringify(user)}</h1>
</div>
Is it possible to do it this way, or do I need a different approach/routing library?
I think it okay to do this. You can get the {user.username} in users/[slug].svelte by export the load function with 'page' parameter. You may try to modify it as below. You may check out the svelteKit online document here
export const load = ({ page }) => {
var username = page.params.slug; //slug refer to [slug].svelte
return {
props: {
user: GetUserByName(username);
}
};
...
You can use the 'query' param
// home.svelte
//use a query string converter library
<a href="users/{user.username}?{objectToQuery(user)}" sveltekit:prefetch/>
then in load function
return {
props: {
slug: page.params.slug,
user: queryToObject(page.params.query)
}
}
but your safest bet is to use a store.
<div>
<h1>{slug}</h1>
<h1>{JSON.stringify($user)}</h1>
</div>
In which case you dont need to pass anything.

Passing location props to page generates build error - Link Component - Gatsby

Reading the documentation I discovered that Gatsby has built-in feature to pass props a to page within its Link Component using the state prop. In development environment everything works as expected and any page is always rendered properly with passed props when navigating from the component below. However during the build I get an error Building static HTML failed for path "/page/", WebpackError: TypeError: Cannot read property 'access' of undefined, same happens for any location.state called inside that page.
Link Component
const data = {
title: "Hello Guys",
date: "23 November 2020"
}
<Link
to="/form"
state={{
access: true,
title: data.title,
date: data.date
}}
> Proceed
</Link>
Page
const Page = ( { location } ) => {
if (location.state.access === true) {
return (
<>
<div>{location.state.title}</div>
<div>{location.state.date}</div>
</>
)}
else {
return <div>Nada</div>
}
}
export default Page
Your issue appears because you are not providing location (hence not state or access properties) in all calls of Page component so the compilation fails.
Add a condition such:
const Page = ( { location } ) => {
if (location && location.state && location.state.access) {
return (
<>
<div>{location.state.title}</div>
<div>{location.state.date}</div>
</>
)}
else {
return <div>Nada</div>
}
}
export default Page
If you are using the Optional Chaining Proposal you just can:
if (location?.state?.access) {...}
You can also set as default the location in the props destructuring like:
const Page = ( { location = {} } ) => {}
So, if you are not sending it, it will result as an empty object ({}), so without any state property inside.

ReactJS: how to manage the state correctly in my case

So I have a container (simplified version), it works as following:
Step1: get the config version
Step2: read the config file above from S3 using the version from
Step3: once the version is loaded, set a flag isLoadLiveConfigComplete -> true, so I know I can render
Step4: setup bunch of tabs to render depending on which one gets clicked
Step5: render a specific tab
The problem is the state.challenges in step5 is undefined when I try to render the tab. But I have checked that in step3 it has content already.
So I’ve read that setState works in an asynchronized manner, which means I cannot use it in a sequential way. But I have checked that data in step3 and state.challenges have been set, and I don’t render the tabs until the flag has been checked and set. Why this is still not working? Any suggestions on how to fix this in this case?
version 2 - used callbacks in 2 places:
class CLL_AppContainer extends Component{
constructor(props) {
super(props);
this.state = { // initial state
tab:CLL_Constants.TABNAME_CHALLANGES,
isLoadLoginDataComplete:false,
isLoadLiveConfigComplete: false,
challenges: [],
configVersion: ""
};
}
componentDidMount() { // loads into browser page
this.getLiveName(); // step1: get the configVersion
…
}
onGetLiveName(data){
// step2: use a callback here to make sure readFromS3() is called after configVersion is set first
this.setState({liveConfig:data.version}, () => {this.readFromS3(this.state.configVersion)});
}
readFromS3(version){
var form = new FormData();
…
form.append('configName', this.state.liveConfig);
…
// step3: read the config file from S3
// fetchAjax() accepts a callback for when the response.success to further process the returned data
fetchAjax(form, (data) => {
console.log(data.configString); // yup I can see the data loaded from S3
this.setState({challenges: data.configString.types});
this.setState({isLoadLiveConfigComplete: true} );
})
}
render() {
var state = this.state;
if(!state.isLoadLoginDataComplete && !state.isLoadLiveConfigComplete) {
return <Spinner />
}
// step4: setup bunch of tabs to render depending on which one gets clicked
return (
<div>
<div align="center">
<button type="button" className="btn btn-default" onClick={() => this.setState({tab: CLL_Constants.TABNAME_CHALLANGES})}>
{CLL_Constants.TABNAME_CHALLANGES}
</button>
…
</div>
<div>
{this.renderTab()}
</div>
</div>
);
}
renderTab() {
var challenges = this.state.challenges; // console.log() shows this is undefined, why???
switch (this.state.tab)
{
case CLL_Constants.TABNAME_CHALLANGES:
return (
<CLL_ChallangesContainer
challenges={challenges}
…
</CLL_ChallangesContainer>
);
…
default:
return (
<Spinner />
);
}
}
}
Truly, there is a way to "force synchronization" while using the setState. This can be achieved using the setState's call back:
this.setState({.... }, () => { *do after the setState is done* })
Some people says this could get hard to understand but
() => { *do after the setState is done* }
is just a function that you could pass as the value for a variable. Get to understand this part and the whole world of React will be at your toes!
Note: While using the mentioned callback be also careful while using "this".
Try also doing:
.....
const self = this
fetchAjax(form, (data) => {
console.log(data.configString); // yup I can see the data loaded from S3
self.setState({challenges: data.configString.types, isLoadLiveConfigComplete: true});
})
....
Be careful with the use of "this"

Resources