- https://relay.dev/docs/debugging/relay-devtools/
-
Relay DevTools
- https://chromewebstore.google.com/detail/relay-developer-tools/ncedobpgnmkhcmnnkcimnobpfepidadl
-
Relay Developer Tools
-
Adds Relay debugging tools to the Chrome Developer Tools. Inspect data in the store of your Relay apps, and how that data changes over time in response to GraphQL queries, and client mutations.
-
- https://github.com/relayjs/relay-devtools
- https://mrtnzlml.com/docs/relay#relay-developer-tools
-
Alternatively, you can also access the store directly (similarly to how the devtools are doing) from your dev console. To do so, you simply need to register a
__RELAY_DEVTOOLS_HOOK__
before creating your Relay Environment -
The usage is simple: call
__RELAY__.store()
from your console or call it with some record ID which is in the store__RELAY__.store('client:root')
. Both of these calls should print the Relay Store content or the single record content respectively.
-
- https://relay.dev/docs/api-reference/store/
-
The Relay Store can be used to programmatically update client-side data inside updater functions. The following is a reference of the Relay Store interface.
- https://relay.dev/docs/api-reference/store/#getdataid-string-recordproxy
- https://relay.dev/docs/api-reference/store/#recordproxy
-
- https://artsy.github.io/blog/2021/04/15/accessing-the-relay-store/
-
Accessing the Relay Store Without a Mutation
-
- https://yarokuk.com/relay-store
-
What is the Relay Store and How to Access/Update the Data in it? (In-Depth Explanation)
-
- https://yashmahalwal.medium.com/a-deep-dive-into-the-relay-store-9388affd2c2b
-
A deep dive into the Relay store
-
-
- https://refine.dev/blog/meta-stylex/
- https://github.com/facebook/stylex
-
StyleX is the styling system for ambitious user interfaces.
-
- https://stylexjs.com/
-
The styling system that powers facebook.com, instagram.com, whatsapp.com, threads.net
-
- https://stylexjs.com/docs/learn/
-
StyleX is a simple, easy-to-use JavaScript syntax and compiler for styling web apps.
StyleX combines the strengths and avoids the weaknesses of both inline styles and static CSS. Defining and using styles requires only local knowledge within a component, and avoids specificity issues while retaining features like Media Queries. StyleX builds optimized styles using collision-free atomic CSS which is superior to what could be authored and maintained by hand.
-
- https://stylexjs.com/playground/
- https://github.com/facebook/stylex
In Relay DevTools, while viewing:
Some interesting paths:
Story
->.message
(TextWithEntities
) ->__ref
(client: 2:message
) ->.text
Story
->.message(location:"homepage_stream")
(TextWithEntities
) ->__ref
(client: 2:message(location:"homepage_stream")
) ->.text
findAllReactElements(({ props, fiber, element }) => {
return props?.children?.props?.hasOwnProperty('feedEdge')
}).map(el => {
const props = getReactProps(el)
return props?.children?.props?.feedEdge?.node
})
// __id
// __typename: Story
// This is basically a wrapper around a Map
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource()
// This gives us all of the record IDs
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().getRecordIDs()
// We can do a crude search through them if we want
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().getRecordIDs().filter(id => id.toLowerCase().includes('story'))
// Or access a record by it's ID
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().get('UzpfSUZTOjE6LTIxNTE1MTA2NzU0ODcxMTY2NDI6ZUp3VG0zbjIrTjhmUzQrdUROS1VFZHR6ZmVyaFcvdGJaRVc1eEdhMWJyajdlSSt1MkxtSmt4N3hpSjJaTk9rUmo2V01TQitMR29NcFdNanNSOStqUDg5dmYyU0hhMHFEYWxrdDRuYUFHY3BtZ2ttMk1mZWR2clhvZDg4NmlWT01USitZVGpBZVluWjR4d3lUbmNJUzhJM3RBdU14RG9ZZlhBNHp1QzNlY1o5aWJCTTh3YmpDZ09tUWdjbUtPcGk2TjZzWkdXNHg3Z3Yrd0RUcHliLzlJQkZwSm84cERVeE1xWG5mbXBrRVVpdEtVb3Z5RW5QaWkxS0xNeEtMVXVGT2syYjBuTmZJTEhaczdwdVRkKy8vazJSZ0VHZUFBbFVoYVVZM0taZzZNM2hncERLSU12RjRpVEhFQ0FreE1DU1pNakFBQUFjNWh2UT0=')
// Theoretically we should be able to get a RecordProxy that would allow us to navigate the links..
// https://relay.dev/docs/api-reference/store/#recordproxy
// TODO: figure out how to do that..
// But, we can always do it manually..
messageRef = __RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().get("UzpfSTM5MTMxMzg2NzY3MzE2NDpTY2FfSUZTY2ExY2EtMjE1MTUxMDY3NTUzMDEyMDA3N2NhZUp4RlUyMUlrd0VRdnJ0M2pUVFJHdk10bW83QXBaUi9wRkFTKzZHUUdtRmYrS2NjZndsaTZrU0F1Uk9wUDVFb2RZOG1ZTm14K3BqbHNUQnM1NWxwQmNjNG1zYWJicE1BbFpvTEUwNUUybTZSaGlIeWh6M3YwNTdwNDdIdTZlWXhkYjV6a3IxOTZjY2ZpYU5IVFFIM0w4ZFAwYVlWR0IzSGZQVGhEcGVKdVJ1Um5lV2t2TEMvY2NPZm1ycnlEbUNLaU91THZmMmZhS2loaEJjY0wrWE5QeDllN1JqZzg3TVhFT0RCNWJ3REJKdTJYclRuN1VaUTlaeWdBM2RId2NjK09PRWNjTTB1TEgxYnE5cGc3eTR4c1JubURQQWttRVZtSFBJem9KTkNPTk1vZnFZRjZlUWhsbXJRTTIwUFdyczlBb2ovRGtNbDRTNk5QYU9YMC80RTJMd2lzRWpoaGt4Qk1Rd0o0WjVNYmlGaVg4VklYU3c0R1RKeG9MN29vcm5rVnBlcnN4d3JnZkJjY2xyT0R1Rkc5bXF2Zm1CcStUM2ZWRWkyUW1vcDRQNXNmeHR0ZTh1MzhyVFQvZlpBOElKSi9FTDErSVNCMTRmbWgzR1N2aVlFWnpQQ3RCYkJwMFdZMFNJRXRBamJlb1NvSGlHaVIxaXpJM2hHRVliZklDdzNJcXkyWWlmVFBZb3c2RUN3T0JBTURnVHJaeno3ZWhiQk5vdlE0a0xvN1VIbzYwSGdoaEEwcnhCOEh4RVdzRDNCSTdua2xsQkFjY210ZXNvaldmRFNlcDBoS01PVnJOWG1UUStpNEdSQzBkYUNQSWxrLzA5MU03RVpjY0JwcVUwSmFKMFN4U0R5QUV4eEJXM1FqZEh4Qyt5MmlpTXJZTHN3ZEQzUHJLczU1SjJoNURpaXEyMjhOTFNWUDNlSWI3d1lXQWNjeXQ1cHlHR2JQSWpxZnhJV2orUzJvK2s5eVBwL0VqVFhpU0xpSXhIek14NGpvbUpKbFp3L0oxZEtmU1dxVTdwazZtN0xFYnBaSHlEYVh0U0VjY1R5ckk0bmpNbE94TDlzVUJISmE5V05sSFMxdUx5OHFMUlllcU9vdEhoUVE0ZXUxOStSMTQyclNTU3JxcXF1cjFiV3ltcWtkK3VVaXVvYXViU3lUaTdiVFprMHhMdW1ySlczTkJFcDZnNVVMQUtBNHhDejB4SVJVM0xBeHBhZ016clYra25ORU9OS29MejRzOGdoTlRIcENnc1ZBZ0hBN1Z5QS8vN2FlaTQ9OmFjYTFjYXtpY2ExMjIxMDE3MDk0ODAyODI2NDM7c2NhOTFjYSJBSUBBUUlnY2NhYjdlYmIzMVpkVzRzeXRSRVVqMUxaX1hYM2U0aV81d3p4NEtSNmNjZUZ4bFRYdmEwVF9JMGUzNUtvSlpSVV9ncDB3ay1BeHdzUk9KdHVVZVFVY2NEIjt9").message.__ref
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().get(messageRef)
Given a page such as:
This will attempt to extract the Story ID's of the posts on the page, then look them up in the Relay store to access the related data:
findAllReactElements(({ props, fiber, element }) => {
return props?.children?.props?.hasOwnProperty('feedEdge')
}).map(el => {
const props = getReactProps(el)
const relayId = props?.children?.props?.feedEdge?.node?.__id
const relayStore = __RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource()
const story = relayStore.get(relayId)
const storyActors = story.actors.__refs.map(actorRef => {
const actor = relayStore.get(actorRef)
return {
id: actor.id,
last_active_time: actor.last_active_time,
name: actor.name,
url: actor.url,
__actor: actor,
}
})
const storyMessage = relayStore.get(story?.message?.__ref)
const storyShareable = relayStore.get(story?.shareable?.__ref)
return {
actorName: storyActors?.[0]?.name,
actors: storyActors,
text: storyMessage?.text,
shareable: storyShareable,
sponsoredData: story.sponsored_data,
creationTime: new Date(story.creation_time*1000).toString(),
url: story.url,
postId: story.post_id,
storyId: story.__id,
__story: story,
__storyMessage: storyMessage,
__storyShareable: storyShareable,
}
}).filter(story => !story.sponsoredData)
News feed posts section:
// News Feed posts
$$('[aria-label="Feeds"] > div > div > div > div > div:nth-child(2) > div > div > div > h2')[0].innerText
// Post Container
postsContainer = $$('[aria-label="Feeds"] > div > div > div > div > div:nth-child(2) > div > div > div > h2 + div')
// News Feed posts
Array.from($$('[aria-label="Feeds"] h2')).map(h2 => h2.innerText)
Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")
// Post Container
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling
postsContainer.children // 18
postsContainer.querySelectorAll('div > [aria-labelledby]') // 15
postsContainer.children[0].querySelectorAll('div > div > div > div > div > div > div > div > [aria-labelledby]')[0].attributes['aria-labelledby'].value
postsContainer.children[0].querySelectorAll('div > div > div > div > div > div > div > div > [aria-labelledby]')[0].attributes['aria-describedby'].value.split(' ').map(id => document.getElementById(id)).filter(Boolean)
postsContainer.children[0].querySelectorAll('div > [aria-labelledby]')[0].attributes['aria-labelledby'].value
postsContainer.children[0].querySelectorAll('div > [aria-labelledby]')[0].attributes['aria-describedby'].value.split(' ').map(id => document.getElementById(id)).filter(Boolean)
Array.from(postsContainer.children[1].querySelectorAll('[dir="auto"]')).map(el => el.innerText).filter(text => !['Facebook', 'Like', 'Comment', 'Share'].includes(text))
Feed Item Person/Page name (seemingly doesn't always match):
labelId = postsContainer.children[0].querySelectorAll('div > [aria-labelledby]')[0].attributes['aria-labelledby'].value
document.getElementById(labelId).querySelector('span > strong a').innerText
Array.from(postsContainer.querySelectorAll('div > [aria-labelledby]'))
.map(el => el.attributes['aria-labelledby'].value)
.filter(Boolean)
.map(id => document.getElementById(id)?.innerText)
React Props/Fibre:
// Function to get find all elements connected to React
function findAllReactElements(customPredicate = () => true, rootNode = document, tagQuery = '*') {
if (typeof customPredicate !== 'function') throw 'customPredicate must be a function returning a boolean'
if (typeof rootNode?.['querySelector'] !== 'function') return [];
const foundElements = rootNode.getElementsByTagName(tagQuery);
return Array.from(foundElements).filter(element => {
const keys = Object.getOwnPropertyNames(element);
const reactPropsKey = keys.find(key => key.startsWith('__reactProps$'));
const reactFiberKey = keys.find(key => key.startsWith('__reactFiber$'));
const props = reactPropsKey ? element[reactPropsKey] : null;
const fiber = reactFiberKey ? element[reactFiberKey] : null;
return (props || fiber) && customPredicate({ props, fiber, element });
});
}
// Function to get React props for a given HTML element
function getReactProps(element) {
const propsKey = Object.getOwnPropertyNames(element).find(propName =>
propName.startsWith('__reactProps$')
);
return propsKey ? element[propsKey] : null;
}
// Function to get React fiber for a given HTML element
function getReactFiber(element) {
const fiberKey = Object.getOwnPropertyNames(element).find(propName =>
propName.startsWith('__reactFiber$')
);
return fiberKey ? element[fiberKey] : null;
}
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling
foundReactElements = findAllReactElements(({ props, fiber, element }) => {
return props?.children?.props?.type === 'body3'
}, postsContainer.children[0])
foundReactElements.map(el => el.innerText)
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling
foundReactElements = findAllReactElements(({ props, fiber, element }) => {
return fiber?.return?.elementType?.displayName?.includes('BaseCometTextWithEntities.react')
}, document)
foundReactElements.map(el => el.innerText)
``
```js
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling
foundReactElements = findAllReactElements(({ props, fiber, element }) => {
return props?.children?.type?.displayName === 'TetraText'
}, postsContainer.children[0])
foundReactElements.map(el => el.innerText).filter(text => text !== 'Facebook')
getReactProps($0).children.map(child => child.type.displayName)
[
"a [from CometFeedStoryCopyrightViolationHeaderSection.react]",
"a [from CometFeedStoryHeaderSection.react]",
"a [from CometFeedStoryContextSection.react]",
"a [from CometFeedStoryPostInformTreatmentSection.react]",
"a [from CometFeedStoryAYMTFooterSection.react]",
"q [from CometPostInformTreatmentContext]",
"a [from CometFeedStoryCallToActionSection.react]",
"a [from CometFeedStoryFeedbackSection.react]",
"a [from CometFeedStoryOuterFooterSection.react]"
]
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling
foundReactElements = findAllReactElements(({ props, fiber, element }) => {
const displayName = fiber?.return?.elementType?.displayName
return [
'BaseCometTextWithEntities.react',
'CometTextWithEntitiesBase.react',
'CometEmoji.react',
'CometImage.react',
'BaseImage.react',
].some(n => displayName?.includes(n))
}, document)
function renderReactElement(element) {
const props = getReactProps(element) || {};
const children = Array.isArray(props?.children) ? props.children : [props?.children]
const { text, alt } = props
const renderedSelf = (text || alt) ? { text, alt } : null
return [
renderedSelf,
...children.map(renderChild),
].flat().filter(Boolean)
}
function renderChild(child) {
if (!child) return null;
const props = child?.props || {}
const displayName = child?.type?.displayName || child?.type?.render?.displayName
const children = Array.isArray(props?.children) ? props.children : [props?.children]
const { text, alt } = props
const renderedSelf = (displayName || text || alt) ? { displayName, text, alt } : null
return [
renderedSelf,
...children.map(renderChild)
].flat().filter(Boolean)
// if (child.type && child.props) {
// if (child.type.displayName?.includes('CometTextWithEntitiesBase.react') ||
// child.type.displayName?.includes('BaseCometTextWithEntities.react')) {
// return child.props.text;
// }
// else if (child.type.render?.displayName?.includes('CometImage.react')) {
// return child.props.alt;
// }
// else {
// // Recursive call to handle nested components or elements
// return renderReactElement(child);
// }
// }
// return null;
}
foundReactElements.map(element => {
// const fiber = getReactFiber(element)
// const props = getReactProps(element)
return renderReactElement(element)
// const children = Array.isArray(props?.children) ? props.children : [props?.children]
// return children.map(child => {
// if (child?.type?.displayName?.includes('CometTextWithEntitiesBase.react') || child?.type?.displayName?.includes('BaseCometTextWithEntities.react')) {
// return child?.props?.text
// }
// else if (child?.type?.render?.displayName.includes('CometImage.react')) {
// return child?.props?.alt
// }
// else {
// return child
// }
// })
}).flat()