<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Marcus Hellberg]]></title><description><![CDATA[Marcus is a long-time Java and web developer. He's curious to learn technologies and excited to share his insights with others. He is the VP of DevRel at Vaadin]]></description><link>https://marcushellberg.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 22:04:57 GMT</lastBuildDate><atom:link href="https://marcushellberg.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Measuring DevRel impact: beyond the metrics]]></title><description><![CDATA[Proving business value remains a long-standing challenge for DevRel teams and leaders. I've heard anecdotes of DevRel professionals spending over half their working hours justifying their roles. That's clearly not great. This lack of trust can become...]]></description><link>https://marcushellberg.dev/measuring-devrel-impact-beyond-the-metrics</link><guid isPermaLink="true">https://marcushellberg.dev/measuring-devrel-impact-beyond-the-metrics</guid><category><![CDATA[DevRel]]></category><dc:creator><![CDATA[Marcus Hellberg]]></dc:creator><pubDate>Mon, 04 Sep 2023 14:04:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693836370714/65b86d76-873d-41ea-8d33-b626daedce7c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Proving business value remains a long-standing challenge for DevRel teams and leaders. I've heard anecdotes of DevRel professionals spending over half their working hours justifying their roles. That's clearly not great. This lack of trust can become a self-fulfilling prophecy that undermines performance.</p>
<p><img src="https://media.giphy.com/media/b7MdMkkFCyCWI/giphy.gif" alt="What would you say you do here?" class="image--center mx-auto" /></p>
<h2 id="heading-devrel-as-a-force-multiplier">DevRel as a force multiplier</h2>
<p>Many DevRel professionals have explored how to <a target="_blank" href="https://www.swyx.io/measuring-devrel">measure DevRel impact</a>. It all boils down to <a target="_blank" href="https://theworst.dev/developer-advocates-guide-to-metrics-and-reporting">tying DevRel activities to established metrics</a> <a target="_blank" href="https://www.marythengvall.com/blog/2020/12/14/first-understand-the-company-goals">within the organization</a>. The complexity arises from DevRel's role as a supporting function that amplifies the efforts of other departments.</p>
<h2 id="heading-time-horizon-mismatch">Time horizon mismatch</h2>
<p><a target="_blank" href="https://www.linkedin.com/posts/marcushellberg_devrel-is-a-long-game-marketing-finds-leads-activity-7081690769878650880-0mtU/">DevRel works on a different time horizon compared to sales and marketing</a>. While the sales team focuses on closing immediate deals and marketing seeks current leads, DevRel lays the groundwork for future leads and sustained growth. This difference in focus can lead to organizational friction.</p>
<h2 id="heading-the-long-term-impact-of-devrel">The long-term impact of DevRel</h2>
<p>Tim Berglund, VP of DevRel at StarTree, wrote a great <a target="_blank" href="https://twitter.com/tlberglund/status/1623366981469753344?s=20">Twitter thread</a> outlining the vital part DevRel plays in helping developer products succeed:</p>
<blockquote>
<p>Developers in general are averse to anything that looks like marketing. If we think a salesperson will call as a result of some interaction, we'll likely go the other way. We want useful information, not a pitch. We run ad blockers.</p>
<p>Today it's farfetched to think an executive might make a significant technology adoption decision without buy-in from at least a layer of senior architects, who in turn, in all but the most dysfunctional cases, have buy-in from developers on the line.</p>
<p>Those developers are bought in because they're already using the thing the company is considering buying. They've been playing with, or even seriously building on, an open-source or cloud free-tier version of the technology for months or years.</p>
<p>They've validated some kind of "ROI" before the "I"—at least the directly monetary part of it. This is how software gets bought now: we get to try it first. This is good!</p>
</blockquote>
<h2 id="heading-a-different-perspective-on-devrel-value">A different perspective on DevRel value</h2>
<p>Rather than solely focusing on tracing a direct line from DevRel activities to sales, consider how many deals have been closed without developer buy-in. The answer is likely very few. DevRel, product, sales, and marketing are all part of the same ecosystem that enables long-term business success.</p>
<h2 id="heading-aligning-devrel-with-business-goals">Aligning DevRel with business goals</h2>
<p>For DevRel to be valued, it needs to contribute to the business objectives. This involves clear communication and alignment with product, marketing, and sales teams.</p>
<p>Value isn't always strictly quantifiable; it can be as qualitative as deepening a customer relationship through a lunch-and-learn session or providing invaluable product feedback. What's important is that others genuinely get value out of your work.</p>
<p>Matt Asay, VP of DevRel at MongoDB, hits the nail on the head in his recent <a target="_blank" href="https://twitter.com/mjasay/status/1697730162056688038">Twitter thread</a>:</p>
<blockquote>
<p>If you're in DevRel and wondering how to measure your value, the first step is to understand your company's objectives. Your job is to directly contribute to them in the most scalable and effective ways possible, working with Product, Sales, Marketing, etc.</p>
<p>If those teams don't think you're indispensable to their success, then there's a disconnect <em>you</em> need to fix. I find DevRel teams often operate in isolation of the business, and then wonder why their busyness isn't appreciated.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[The real power of AI: getting from idea to reality quicker]]></title><description><![CDATA[It's remarkable how quickly you can go from an idea to production with the help of AI tools.
Earlier this week, I had a thought: what could I learn about trending topics in Java if I analyzed accepted talks at Java conferences? Thanks to ChatGPT and ...]]></description><link>https://marcushellberg.dev/the-real-power-of-ai-getting-from-idea-to-reality-quicker</link><guid isPermaLink="true">https://marcushellberg.dev/the-real-power-of-ai-getting-from-idea-to-reality-quicker</guid><category><![CDATA[AI]]></category><category><![CDATA[writing]]></category><category><![CDATA[chatgpt]]></category><dc:creator><![CDATA[Marcus Hellberg]]></dc:creator><pubDate>Fri, 30 Jun 2023 18:36:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/npxXWgQ33ZQ/upload/c7b3a7e2cf8aa876134574bbfa580fdd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's remarkable how quickly you can go from an idea to production with the help of AI tools.</p>
<p>Earlier this week, I had a thought: what could I learn about trending topics in Java if I analyzed accepted talks at Java conferences? Thanks to <a target="_blank" href="https://chat.openai.com/chat">ChatGPT</a> and <a target="_blank" href="https://github.com/features/copilot">Copilot</a>, I was able to create the analysis and publish my <a target="_blank" href="https://marcushellberg.dev/java-ecosystem-trends-report-2023">Java Ecosystem Trends Report 2023</a> less than a day after getting the idea.</p>
<p>I had all the skills needed to write the scraper, categorize data, and analyze the result. What I didn't have was a week to do it all by hand. Working with AI tools, I could translate my idea into reality in a fraction of the time.</p>
<h2 id="heading-heres-how-i-used-ai-tools-to-ship-my-idea-faster">Here's how I used AI tools to ship my idea faster</h2>
<p>Here's what I wanted to do: find out which topics were most frequently present in accepted conference sessions.</p>
<p>I had a rough idea of the steps I needed to take:</p>
<ol>
<li><p>Scrape conference data from websites</p>
</li>
<li><p>Categorize the data into distinct topics</p>
</li>
<li><p>Analyze the data to find out the most common topics and any other interesting data</p>
</li>
<li><p>Write a report</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688149861499/6ccffee9-c34a-4ebd-81f1-309daa43fc4c.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-scrape-data">Scrape data</h3>
<p>I began by asking ChatGPT to write me a web scraper script in Node based on what I was trying to accomplish. I went back and forth between coding in my IDE to asking ChatGPT to improve certain aspects of the script until I had something that worked for all the conferences I wanted to analyze. The output was a JSON file with session titles and abstracts per conference.</p>
<h3 id="heading-categorize-data">Categorize data</h3>
<p>Next, I asked ChatGPT to help me write a Node script that takes the JSON file as input and sends each title+abstract to the OpenAI chat completion API to get classified. Again, I had to go back and forth several times. First, to work on the prompt, then to batch the requests and show a progress indicator as the process took a long time. I used the new <a target="_blank" href="https://platform.openai.com/docs/guides/gpt/function-calling">OpenAI function calling</a> mechanism to <a target="_blank" href="https://yonom.substack.com/p/native-json-output-from-gpt-4">ensure I got valid JSON as output</a>. The output was a CSV file with 3 columns: <code>conference</code>, <code>topic</code>, and <code>count</code>.</p>
<h3 id="heading-analyze-data">Analyze data</h3>
<p>I performed the analysis step in a Jupyter Notebook using Python, Pandas, and Seaborn. I worked with ChatGPT to quickly do the analysis and create charts. I liked having the Python code allowed me to verify the work (as opposed to using a more automated AI data analysis tool).</p>
<h3 id="heading-write-the-report">Write the report</h3>
<p>With the analysis done, the last remaining part was drawing conclusions and writing up a report. I already had some takeaways, but I still asked ChatGPT to look at the list of most common topics to see if it could develop other insights. Most of the insights were aligned with what I was already thinking, and others weren't that valuable. But some of its insights helped spark more ideas for me.</p>
<p>Finally, I wrote the report and hit publish. The <a target="_blank" href="https://marcushellberg.dev/java-ecosystem-trends-report-2023">2023 Java Ecosystem Trends Report</a> was born. I purposefully published it early as an <a target="_blank" href="https://en.wikipedia.org/wiki/Minimum_viable_product">MVP</a> instead of trying to make it perfect. It has allowed med to get great feedback and insights on how to make the categorization more robust for next time.</p>
<h2 id="heading-key-takeaways">Key takeaways</h2>
<ul>
<li><p>Know what you're trying to accomplish before you start. If not, you'll end up on a wild goose chase.</p>
</li>
<li><p>Work in small steps. Give enough context and iterate until you get the result you need.</p>
</li>
<li><p>Verify the results. Just because something is a statistically probable answer to your question doesn't mean it's right. You should be able to stand behind what you create.</p>
</li>
<li><p>Ship early and gather feedback.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Java ecosystem trends report 2023]]></title><description><![CDATA[As a frequent conference speaker, I was curious to know what the hot topics are in the Java ecosystem. To find out, I analyzed accepted talks at four large Java conferences held in 2023: JFokus, DevNexus, Devoxx UK, and JCON Europe.
The most popular ...]]></description><link>https://marcushellberg.dev/java-ecosystem-trends-report-2023</link><guid isPermaLink="true">https://marcushellberg.dev/java-ecosystem-trends-report-2023</guid><category><![CDATA[Java]]></category><category><![CDATA[report]]></category><category><![CDATA[trends]]></category><dc:creator><![CDATA[Marcus Hellberg]]></dc:creator><pubDate>Thu, 29 Jun 2023 16:39:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/esTP6gqfm54/upload/4a0b61ae6513a8322710f0e67d6b447c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a frequent conference speaker, I was curious to know what the hot topics are in the Java ecosystem. To find out, I analyzed accepted talks at four large Java conferences held in 2023: <a target="_blank" href="https://www.jfokus.se/">JFokus</a>, <a target="_blank" href="https://devnexus.com/">DevNexus</a>, <a target="_blank" href="https://www.devoxx.co.uk/">Devoxx UK</a>, and <a target="_blank" href="https://2023.europe.jcon.one/">JCON Europe</a>.</p>
<h2 id="heading-the-most-popular-topics-in-the-java-ecosystem">The most popular topics in the Java ecosystem</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688052645015/d80b79c9-5956-4bda-9a89-f7ebc95b5766.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-key-findings">Key findings</h2>
<ol>
<li><p><strong>Java development is increasingly cloud-native.</strong> The prominence of Microservices, Kubernetes, Quarkus, Serverless, Kafka, and Containerization indicates that the Java ecosystem is significantly moving towards distributed and cloud-native applications. This shift is likely driven by the demands for better scalability, maintainability, and business agility. This supports the findings in the <a target="_blank" href="https://vaadin.com/java-report-2023">2023 State of Java in the Enterprise</a> report by <a target="_blank" href="http://vaadin.com/">Vaadin</a>.</p>
</li>
<li><p><strong>AI and ML are gaining traction.</strong> AI and Machine Learning are causing a big disruption in the software industry and the Java ecosystem is not excluded. Developers are using AI tools for development and integrating AI/ML in the applications they are building. Much of AI/ML tooling is only available for Python and JavaScript. We can expect to see new projects emerge to allow Java developers to take advantage of AI/ML more effectively.</p>
</li>
<li><p><strong>Solid software development practices continue to be important.</strong> The continued mention of topics like Security, Refactoring, and Testing underscores that, even amidst fast-evolving tech trends, the fundamentals of writing secure, maintainable, and well-tested code remain critical. This illustrates the persistent need for robust software development practices, irrespective of the technology or architecture used.</p>
</li>
<li><p><strong>Development is increasingly complex.</strong> The wide array of topics from different domains (e.g., Microservices, AI/ML, Kubernetes, Security) shows that the Java development environment is becoming increasingly complex. Developers are increasingly expected to do DevOps and need to understand and integrate a growing number of technologies and concepts, requiring a broader skill set and continuous learning.</p>
</li>
</ol>
<h2 id="heading-comparing-conferences">Comparing conferences</h2>
<p>Looking at the aggregate data alone can give us a false sense of uniformity. By looking at the frequency of topics across conferences, we can see there is quite a difference between events. AI/ML was so prevalent at Devoxx UK, that it can make it hard to see the nuances between other topics.</p>
<p>The conferences are listed in chronological order.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688053073008/46b8228e-de73-4e5a-8b7b-612b662d32b7.jpeg" alt class="image--center mx-auto" /></p>
<p>By removing the AI/ML topic from the analysis, we can better see the popularity of the remaining top 19 topics.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688053083788/34dfd128-6bbb-4183-a7fe-0e9c87f97870.jpeg" alt class="image--center mx-auto" /></p>
<p>JFokus stands out in the analysis, having fewer categories with multiple talks. We can also confirm this by looking at the distribution of topic frequencies at the events. JFokus has significantly more topics with only one mention, meaning it had a more diverse set of talks than the other conferences.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688052679085/0562d2bb-3b00-4492-bf4a-306433220d11.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-methodology">Methodology</h2>
<p>I gathered the data from the conference websites on June 28, 2023.</p>
<p>I classified the topics by feeding the titles and abstracts to the OpenAI chat completion API with a prompt asking it to extract the 3 most relevant topics that describe the talk, focusing on technologies and methodologies. I used the function API to force the output into JSON format for easier processing.</p>
<p>I manually cleaned the data by combining duplicate topics such as "apache kafka" and "kafka." I removed two overly generic topics: "java" and "software development."</p>
<p>Finally, I analyzed and visualized the data with Pandas and Seaborn.</p>
<p>Because the data represent accepted talks at conferences, we should assume that they skew towards trendy topics that help sell conference tickets.</p>
<p>Learn <a target="_blank" href="https://marcushellberg.dev/the-real-power-of-ai-getting-from-idea-to-reality-quicker">how I used AI tools to turn my idea into a completed report in less than a day</a>.</p>
<h2 id="heading-more-conferences">More conferences?</h2>
<p>Is there a relevant conference missing that you think would add value to the analysis? Let me know in the comments or reach out to me on Twitter <a target="_blank" href="https://twitter.com/marcushellberg">@marcushellberg</a>.</p>
<h2 id="heading-appendix-top-100-topics">Appendix: top 100 topics</h2>
<p>Here is a list of the top 100 topics sorted by popularity</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Topic</td><td>Count</td></tr>
</thead>
<tbody>
<tr>
<td>microservices</td><td>35.0</td></tr>
<tr>
<td>kubernetes</td><td>34.0</td></tr>
<tr>
<td>ai/ml</td><td>33.0</td></tr>
<tr>
<td>spring boot</td><td>24.0</td></tr>
<tr>
<td>security</td><td>20.0</td></tr>
<tr>
<td>quarkus</td><td>19.0</td></tr>
<tr>
<td>devops</td><td>16.0</td></tr>
<tr>
<td>apache kafka</td><td>16.0</td></tr>
<tr>
<td>serverless</td><td>15.0</td></tr>
<tr>
<td>graalvm</td><td>14.0</td></tr>
<tr>
<td>observability</td><td>13.0</td></tr>
<tr>
<td>jakarta ee</td><td>13.0</td></tr>
<tr>
<td>docker</td><td>11.0</td></tr>
<tr>
<td>open source</td><td>11.0</td></tr>
<tr>
<td>cloud</td><td>10.0</td></tr>
<tr>
<td>refactoring</td><td>10.0</td></tr>
<tr>
<td>testing</td><td>9.0</td></tr>
<tr>
<td>spring</td><td>9.0</td></tr>
<tr>
<td>containerization</td><td>9.0</td></tr>
<tr>
<td>distributed systems</td><td>8.0</td></tr>
<tr>
<td>opentelemetry</td><td>8.0</td></tr>
<tr>
<td>ci/cd</td><td>8.0</td></tr>
<tr>
<td>cloud computing</td><td>8.0</td></tr>
<tr>
<td>kotlin</td><td>8.0</td></tr>
<tr>
<td>microstream</td><td>8.0</td></tr>
<tr>
<td>performance optimization</td><td>7.0</td></tr>
<tr>
<td>testcontainers</td><td>7.0</td></tr>
<tr>
<td>virtual threads</td><td>7.0</td></tr>
<tr>
<td>automation</td><td>7.0</td></tr>
<tr>
<td>tools</td><td>7.0</td></tr>
<tr>
<td>pattern matching</td><td>7.0</td></tr>
<tr>
<td>cloud-native</td><td>7.0</td></tr>
<tr>
<td>jvm</td><td>7.0</td></tr>
<tr>
<td>micronaut</td><td>6.0</td></tr>
<tr>
<td>apache pulsar</td><td>6.0</td></tr>
<tr>
<td>performance</td><td>6.0</td></tr>
<tr>
<td>design patterns</td><td>6.0</td></tr>
<tr>
<td>dependency management</td><td>6.0</td></tr>
<tr>
<td>architecture</td><td>6.0</td></tr>
<tr>
<td>collaboration</td><td>5.0</td></tr>
<tr>
<td>migration</td><td>5.0</td></tr>
<tr>
<td>java applications</td><td>5.0</td></tr>
<tr>
<td>developer experience</td><td>5.0</td></tr>
<tr>
<td>scaling</td><td>5.0</td></tr>
<tr>
<td>project management</td><td>5.0</td></tr>
<tr>
<td>web development</td><td>5.0</td></tr>
<tr>
<td>streaming data</td><td>5.0</td></tr>
<tr>
<td>continuous delivery</td><td>5.0</td></tr>
<tr>
<td>code quality</td><td>5.0</td></tr>
<tr>
<td>concurrency</td><td>5.0</td></tr>
<tr>
<td>software architecture</td><td>5.0</td></tr>
<tr>
<td>application development</td><td>5.0</td></tr>
<tr>
<td>cloud native</td><td>5.0</td></tr>
<tr>
<td>openjdk</td><td>5.0</td></tr>
<tr>
<td>continuous integration</td><td>5.0</td></tr>
<tr>
<td>programming languages</td><td>5.0</td></tr>
<tr>
<td>user experience</td><td>4.0</td></tr>
<tr>
<td>software engineering</td><td>4.0</td></tr>
<tr>
<td>typescript</td><td>4.0</td></tr>
<tr>
<td>knative</td><td>4.0</td></tr>
<tr>
<td>learning</td><td>4.0</td></tr>
<tr>
<td>developer productivity</td><td>4.0</td></tr>
<tr>
<td>developer tools</td><td>4.0</td></tr>
<tr>
<td>development</td><td>4.0</td></tr>
<tr>
<td>monitoring</td><td>4.0</td></tr>
<tr>
<td>react</td><td>4.0</td></tr>
<tr>
<td>github actions</td><td>4.0</td></tr>
<tr>
<td>functional programming</td><td>4.0</td></tr>
<tr>
<td>apis</td><td>4.0</td></tr>
<tr>
<td>javascript</td><td>4.0</td></tr>
<tr>
<td>cloud-native applications</td><td>4.0</td></tr>
<tr>
<td>technical debt</td><td>4.0</td></tr>
<tr>
<td>integration testing</td><td>4.0</td></tr>
<tr>
<td>open source projects</td><td>4.0</td></tr>
<tr>
<td>reactive programming</td><td>4.0</td></tr>
<tr>
<td>asynchronous programming</td><td>4.0</td></tr>
<tr>
<td>records</td><td>4.0</td></tr>
<tr>
<td>redis</td><td>4.0</td></tr>
<tr>
<td>community</td><td>4.0</td></tr>
<tr>
<td>change data capture</td><td>3.0</td></tr>
<tr>
<td>cloud native applications</td><td>3.0</td></tr>
<tr>
<td>ci/cd pipelines</td><td>3.0</td></tr>
<tr>
<td>containers</td><td>3.0</td></tr>
<tr>
<td>sql</td><td>3.0</td></tr>
<tr>
<td>concurrent programming</td><td>3.0</td></tr>
<tr>
<td>test-driven development</td><td>3.0</td></tr>
<tr>
<td>communication</td><td>3.0</td></tr>
<tr>
<td>test automation</td><td>3.0</td></tr>
<tr>
<td>technology</td><td>3.0</td></tr>
<tr>
<td>java persistence</td><td>3.0</td></tr>
<tr>
<td>continuous deployment</td><td>3.0</td></tr>
<tr>
<td>software testing</td><td>3.0</td></tr>
<tr>
<td>libraries</td><td>3.0</td></tr>
<tr>
<td>kafka</td><td>3.0</td></tr>
<tr>
<td>metrics</td><td>3.0</td></tr>
<tr>
<td>microprofile</td><td>3.0</td></tr>
<tr>
<td>jdk</td><td>3.0</td></tr>
<tr>
<td>java frameworks</td><td>3.0</td></tr>
<tr>
<td>open source tools</td><td>3.0</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Code is cheap: why good products matter more than ever]]></title><description><![CDATA[Generative AI tools like ChatGPT have transformed the landscape of product development seemingly overnight. They've made it easier and faster to convert ideas into products. But as tools help us write more code quicker, we need to keep our focus on b...]]></description><link>https://marcushellberg.dev/code-is-cheap-why-good-products-matter-more-than-ever</link><guid isPermaLink="true">https://marcushellberg.dev/code-is-cheap-why-good-products-matter-more-than-ever</guid><category><![CDATA[chatgpt]]></category><category><![CDATA[AI]]></category><category><![CDATA[Product Management]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Marcus Hellberg]]></dc:creator><pubDate>Mon, 08 May 2023 23:46:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/npxXWgQ33ZQ/upload/66e847060378317d9fc1007561fe1325.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Generative AI tools like ChatGPT have transformed the landscape of product development seemingly overnight. They've made it easier and faster to convert ideas into products. But as tools help us write more code quicker, we need to keep our focus on building products that effectively address the actual needs of our users.</p>
<h2 id="heading-the-power-of-ai-in-product-development"><strong>The power of AI in product development</strong></h2>
<p>ChatGPT is a powerful tool that can help developers and product teams turn ideas into reality quicker than before. Automating parts of the development process allows developers to focus on the meaningful aspects of their work and tackle impactful issues. In a world where writing code is becoming cheaper, the actual value remains in solving real user problems.</p>
<h2 id="heading-the-key-to-success-is-still-building-good-products"><strong>The key to success is (still) building good products</strong></h2>
<p>As generative AI tools like ChatGPT continue to improve, it becomes even more important for product teams to focus on creating products that address genuine user needs. This means that instead of getting distracted by how fast they can add more features nobody asked for, teams should prioritize understanding their users and discovering the problems that need solving. Focus on quality over quantity.</p>
<h2 id="heading-ai-is-a-tool-not-a-human-replacement"><strong>AI is a tool, not a human replacement</strong></h2>
<p>It's important to remember that AI is not a replacement for developers. But it will inevitably change the way developers work.</p>
<p>With AI tools like ChatGPT, developers can be more impactful by spending more time on the meaningful aspects of their work, such as creating innovative solutions to real-world problems.</p>
<p>I've lost count of how many side projects I've stopped working on because I got bogged down with the drudgery of project setup. With AI tools, I can generate most of the boilerplate needed in minutes and start building features that deliver user value quicker.</p>
<h2 id="heading-entering-the-generative-ai-era-with-a-user-focus"><strong>Entering the generative AI era with a user focus</strong></h2>
<p>As we enter the generative AI era, the key to success will continue to be identifying and focusing on good product ideas that address genuine user needs. AI tools like ChatGPT are here to help, but it's up to us to use them wisely and effectively to create products that truly make a difference.</p>
]]></content:encoded></item><item><title><![CDATA[How to build a custom ChatGPT assistant for your documentation]]></title><description><![CDATA[ChatGPT can be a helpful tool for development and learning. But only if it knows about the technology you're using.
I wanted ChatGPT to help me develop Hilla applications, but since it was released after 2021, instead of giving helpful answers, ChatG...]]></description><link>https://marcushellberg.dev/how-to-build-a-custom-chatgpt-assistant-for-your-documentation</link><guid isPermaLink="true">https://marcushellberg.dev/how-to-build-a-custom-chatgpt-assistant-for-your-documentation</guid><category><![CDATA[chatgpt]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[React]]></category><category><![CDATA[documentation]]></category><dc:creator><![CDATA[Marcus Hellberg]]></dc:creator><pubDate>Tue, 02 May 2023 01:14:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/qGGNt_oqs5M/upload/198980c8f740c016a8fbff0f0588c23e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>ChatGPT can be a helpful tool for development and learning. But only if it knows about the technology you're using.</p>
<p>I wanted ChatGPT to help me develop <a target="_blank" href="https://hilla.dev">Hilla</a> applications, but since it was released after 2021, instead of giving helpful answers, ChatGPT made up answers that had nothing to do with reality.</p>
<p>I also had the challenge that Hilla supports both React and Lit on the front end, and I want to ensure that the answers use the correct framework as context.</p>
<p>Here's how I built an assistant using the most up-to-date documentation as context for ChatGPT to give me relevant answers.</p>
<h2 id="heading-key-concept-embeddings">Key concept: embeddings</h2>
<p>ChatGPT, like other LLMs, has a limited context size that needs to fit your question, relevant background information, and the answer. For example, <code>gpt-3.5-turbo</code> has a limit of 4096 tokens or roughly 3000 words. In order to get relevant answers to your questions, you need to include the most valuable parts of documentation in the prompt.</p>
<p>An efficient way of finding the most relevant parts of the documentation to include is embeddings. You can think of embeddings as a way of encoding the <em>meaning</em> of a piece of text into a vector that describes a point in a multi-dimensional space. Texts with similar meanings are close to each other, while texts that differ in meaning are farther apart.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682963325984/31b3c5ac-2737-44c6-af6e-0c63f7d84400.png" alt="A document on the left gets mapped through a magic black box that outputs a vector representing the text's meaning." class="image--center mx-auto" /></p>
<p>The concept is very similar to a color picker. Each color can be represented by a 3-element vector of red, green, and blue values. Similar colors have similar values, whereas different colors have different values.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683236763927/b1ad84e6-7911-4bdf-911e-ed7960c7ab8f.png" alt="A color picker representing a color as an RGB value" class="image--center mx-auto" /></p>
<p>For this article, it's enough to know that there's an <a target="_blank" href="https://platform.openai.com/docs/guides/embeddings">OpenAI API that turns texts into embeddings</a>. This <a target="_blank" href="https://www.pinecone.io/learn/vector-embeddings/">article on embeddings</a> is a good resource if you want to learn more about how embeddings work under the hood.</p>
<p>Once you have created embeddings for your documentation, you can quickly find the most relevant parts to include in the prompt by finding the parts closest in meaning to the question.</p>
<h2 id="heading-the-big-picture-providing-documentation-as-context-to-chatgpt">The big picture: providing documentation as context to ChatGPT</h2>
<p>Here are the high-level steps needed to make ChatGPT use your documentation as context for answering questions:</p>
<h3 id="heading-generating-embeddings-for-your-documentation">Generating embeddings for your documentation</h3>
<ol>
<li><p>Split your documentation into smaller parts, for instance, by heading, and create an embedding (vector) for each chunk.</p>
</li>
<li><p>Save the embedding, source text, and other metadata in a vector database.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682544880645/007851db-c7fd-4611-b534-7bc7eb5df412.png" alt="A 2D visualization showing different topics on a plane. " class="image--center mx-auto" /></p>
<h3 id="heading-answering-questions-with-documentation-as-context">Answering questions with documentation as context</h3>
<ol>
<li><p>Create an embedding for the user question.</p>
</li>
<li><p>Search the vector database for the N parts of the documentation most related to the question using the embedding.</p>
</li>
<li><p>Construct a prompt instructing ChatGPT to answer the given question only using the provided documentation.</p>
</li>
<li><p>Use the OpenAI API to generate a completion for the prompt.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682544919209/670fdc45-264c-402d-b64d-3069040b90c6.png" alt="A visualization of embeddings on a 2D plane. A cluster of related themes are highlighted as similar to the given question." class="image--center mx-auto" /></p>
<p>In the following sections, I will go into more detail about how I implemented these steps.</p>
<h2 id="heading-tools-used">Tools used</h2>
<ul>
<li><p><a target="_blank" href="https://www.pinecone.io/">Pinecone</a></p>
</li>
<li><p><a target="_blank" href="https://nodejs.org/en">Node.js</a></p>
</li>
<li><p><a target="_blank" href="https://nextjs.org/">Next.js</a></p>
</li>
</ul>
<h2 id="heading-source-code">Source code</h2>
<p>I will only highlight the most relevant sections of the code below. You can find the <a target="_blank" href="https://github.com/marcushellberg/hilla-docs-gpt">complete source code on GitHub</a>.</p>
<h2 id="heading-processing-documentation">Processing documentation</h2>
<p>The <a target="_blank" href="https://github.com/vaadin/docs/tree/hilla/articles">Hilla documentation</a> is in Asciidoc format. The steps needed to process them into embeddings are as follows:</p>
<ul>
<li><p>Process the Asciidoc files with Asciidoctor to include code snippets and other includes</p>
</li>
<li><p>Split the resulting document into sections based on the HTML document structure</p>
</li>
<li><p>Convert the content to plain text to save on tokens</p>
</li>
<li><p>If needed, split sections into smaller pieces</p>
</li>
<li><p>Create embedding vectors for each text section</p>
</li>
<li><p>Save the embedding vectors along with the source text into Pinecone</p>
</li>
</ul>
<p><strong>Asciidoc processing</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processAdoc</span>(<span class="hljs-params">file, path</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Processing <span class="hljs-subst">${path}</span>`</span>);

  <span class="hljs-keyword">const</span> frontMatterRegex = <span class="hljs-regexp">/^---[\s\S]+?---\n*/</span>;


  <span class="hljs-keyword">const</span> namespace = path.includes(<span class="hljs-string">'articles/react'</span>) ? <span class="hljs-string">'react'</span> : path.includes(<span class="hljs-string">'articles/lit'</span>) ? <span class="hljs-string">'lit'</span> : <span class="hljs-string">''</span>;
  <span class="hljs-keyword">if</span> (!namespace) <span class="hljs-keyword">return</span>;

  <span class="hljs-comment">// Remove front matter. The JS version of asciidoctor doesn't support removing it.</span>
  <span class="hljs-keyword">const</span> noFrontMatter = file.replace(frontMatterRegex, <span class="hljs-string">''</span>);

  <span class="hljs-comment">// Run through asciidoctor to get includes</span>
  <span class="hljs-keyword">const</span> html = asciidoctor.convert(noFrontMatter, {
    <span class="hljs-attr">attributes</span>: {
      <span class="hljs-attr">root</span>: process.env.DOCS_ROOT,
      <span class="hljs-attr">articles</span>: process.env.DOCS_ARTICLES,
      <span class="hljs-attr">react</span>: namespace === <span class="hljs-string">'react'</span>,
      <span class="hljs-attr">lit</span>: namespace === <span class="hljs-string">'lit'</span>
    },
    <span class="hljs-attr">safe</span>: <span class="hljs-string">'unsafe'</span>,
    <span class="hljs-attr">base_dir</span>: process.env.DOCS_ARTICLES
  });

  <span class="hljs-comment">// Extract sections</span>
  <span class="hljs-keyword">const</span> dom = <span class="hljs-keyword">new</span> JSDOM(html);
  <span class="hljs-keyword">const</span> sections = dom.window.document.querySelectorAll(<span class="hljs-string">'.sect1'</span>);

  <span class="hljs-comment">// Convert section html to plain text to save on tokens</span>
  <span class="hljs-keyword">const</span> plainText = <span class="hljs-built_in">Array</span>.from(sections).map(<span class="hljs-function"><span class="hljs-params">section</span> =&gt;</span> convert(section.innerHTML));

  <span class="hljs-comment">// Split section content further if needed, filter out short blocks</span>
  <span class="hljs-keyword">const</span> docs = <span class="hljs-keyword">await</span> splitter.createDocuments(plainText);
  <span class="hljs-keyword">const</span> blocks = docs.map(<span class="hljs-function"><span class="hljs-params">doc</span> =&gt;</span> doc.pageContent)
    .filter(<span class="hljs-function"><span class="hljs-params">block</span> =&gt;</span> block.length &gt; <span class="hljs-number">200</span>);

  <span class="hljs-keyword">await</span> createAndSaveEmbeddings(blocks, path, namespace);
}
</code></pre>
<p><strong>Create embeddings and save them</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createAndSaveEmbeddings</span>(<span class="hljs-params">blocks, path, namespace</span>) </span>{

  <span class="hljs-comment">// OpenAI suggests removing newlines for better performance when creating embeddings. </span>
  <span class="hljs-comment">// Don't remove them from the source.</span>
  <span class="hljs-keyword">const</span> withoutNewlines = blocks.map(<span class="hljs-function"><span class="hljs-params">block</span> =&gt;</span> block.replace(<span class="hljs-regexp">/\n/g</span>, <span class="hljs-string">' '</span>));
  <span class="hljs-keyword">const</span> embeddings = <span class="hljs-keyword">await</span> getEmbeddings(withoutNewlines);
  <span class="hljs-keyword">const</span> vectors = embeddings.map(<span class="hljs-function">(<span class="hljs-params">embedding, i</span>) =&gt;</span> ({
    <span class="hljs-attr">id</span>: nanoid(),
    <span class="hljs-attr">values</span>: embedding,
    <span class="hljs-attr">metadata</span>: {
      <span class="hljs-attr">path</span>: path,
      <span class="hljs-attr">text</span>: blocks[i]
    }
  }));

  <span class="hljs-keyword">await</span> pinecone.upsert({
    <span class="hljs-attr">upsertRequest</span>: {
      vectors,
      namespace
    }
  });
}
</code></pre>
<p><strong>Get embeddings from OpenAI</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getEmbeddings</span>(<span class="hljs-params">texts</span>) </span>{
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> openai.createEmbedding({
    <span class="hljs-attr">model</span>: <span class="hljs-string">'text-embedding-ada-002'</span>,
    <span class="hljs-attr">input</span>: texts
  });
  <span class="hljs-keyword">return</span> response.data.data.map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.embedding);
}
</code></pre>
<h2 id="heading-searching-with-context">Searching with context</h2>
<p>So far, we have split the documentation into small sections and saved them in a vector database. When a user asks a question, we need to:</p>
<ul>
<li><p>Create an embedding based on the asked question</p>
</li>
<li><p>Search the vector database for the 10 parts of the documentation that are most relevant to the question</p>
</li>
<li><p>Create a prompt that includes as many documentation sections as possible into 1536 tokens, leaving 2560 tokens for the answer.</p>
</li>
</ul>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getMessagesWithContext</span>(<span class="hljs-params">messages: ChatCompletionRequestMessage[], frontend: string</span>) </span>{

  <span class="hljs-comment">// Ensure that there are only messages from the user and assistant, trim input</span>
  <span class="hljs-keyword">const</span> historyMessages = sanitizeMessages(messages);

  <span class="hljs-comment">// Send all messages to OpenAI for moderation.</span>
  <span class="hljs-comment">// Throws exception if flagged -&gt; should be handled properly in a real app.</span>
  <span class="hljs-keyword">await</span> moderate(historyMessages);

  <span class="hljs-comment">// Extract the last user message to get the question</span>
  <span class="hljs-keyword">const</span> [userMessage] = historyMessages.filter(<span class="hljs-function">(<span class="hljs-params">{role}</span>) =&gt;</span> role === ChatCompletionRequestMessageRoleEnum.User).slice(<span class="hljs-number">-1</span>)

  <span class="hljs-comment">// Create an embedding for the user's question</span>
  <span class="hljs-keyword">const</span> embedding = <span class="hljs-keyword">await</span> createEmbedding(userMessage.content);

  <span class="hljs-comment">// Find the most similar documents to the user's question</span>
  <span class="hljs-keyword">const</span> docSections = <span class="hljs-keyword">await</span> findSimilarDocuments(embedding, <span class="hljs-number">10</span>, frontend);

  <span class="hljs-comment">// Get at most 1536 tokens of documentation as context</span>
  <span class="hljs-keyword">const</span> contextString = <span class="hljs-keyword">await</span> getContextString(docSections, <span class="hljs-number">1536</span>);

  <span class="hljs-comment">// The messages that set up the context for the question</span>
  <span class="hljs-keyword">const</span> initMessages: ChatCompletionRequestMessage[] = [
    {
      <span class="hljs-attr">role</span>: ChatCompletionRequestMessageRoleEnum.System,
      <span class="hljs-attr">content</span>: codeBlock<span class="hljs-string">`
          <span class="hljs-subst">${oneLine<span class="hljs-string">`
            You are Hilla AI. You love to help developers! 
            Answer the user's question given the following
            information from the Hilla documentation.
          `</span>}</span>
        `</span>
    },
    {
      <span class="hljs-attr">role</span>: ChatCompletionRequestMessageRoleEnum.User,
      <span class="hljs-attr">content</span>: codeBlock<span class="hljs-string">`
          Here is the Hilla documentation:
          """
          <span class="hljs-subst">${contextString}</span>
          """
        `</span>
    },
    {
      <span class="hljs-attr">role</span>: ChatCompletionRequestMessageRoleEnum.User,
      <span class="hljs-attr">content</span>: codeBlock<span class="hljs-string">`
          <span class="hljs-subst">${oneLine<span class="hljs-string">`
            Answer all future questions using only the above        
            documentation and your knowledge of the 
            <span class="hljs-subst">${frontend === <span class="hljs-string">'react'</span> ? <span class="hljs-string">'React'</span> : <span class="hljs-string">'Lit'</span>}</span> library
          `</span>}</span>
          <span class="hljs-subst">${oneLine<span class="hljs-string">`
            You must also follow the below rules when answering:
          `</span>}</span>
          <span class="hljs-subst">${oneLine<span class="hljs-string">`
            - Do not make up answers that are not provided 
              in the documentation 
          `</span>}</span>
          <span class="hljs-subst">${oneLine<span class="hljs-string">`
            - If you are unsure and the answer is not explicitly 
              written in the documentation context, say 
              "Sorry, I don't know how to help with that"
          `</span>}</span>
          <span class="hljs-subst">${oneLine<span class="hljs-string">`
            - Prefer splitting your response into 
              multiple paragraphs
          `</span>}</span>
          <span class="hljs-subst">${oneLine<span class="hljs-string">`
            - Output as markdown
          `</span>}</span>
          <span class="hljs-subst">${oneLine<span class="hljs-string">`
            - Always include code snippets if available
          `</span>}</span>
        `</span>
    }
  ];

  <span class="hljs-comment">// Cap the messages to fit the max token count, removing earlier messages if necessary</span>
  <span class="hljs-keyword">return</span> capMessages(
    initMessages,
    historyMessages
  );
}
</code></pre>
<p>When a user asks a question, we call <code>getMessagesWithContext()</code> to get the messages we need to send to ChatGPT. We then call the OpenAI API to get the completion and stream the response to the client.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params">req: NextRequest</span>) </span>{
  <span class="hljs-comment">// All the non-system messages up until now along with </span>
  <span class="hljs-comment">// the framework we should use for the context.</span>
  <span class="hljs-keyword">const</span> {messages, frontend} = (<span class="hljs-keyword">await</span> req.json()) <span class="hljs-keyword">as</span> {
    <span class="hljs-attr">messages</span>: ChatCompletionRequestMessage[],
    <span class="hljs-attr">frontend</span>: string
  };
  <span class="hljs-keyword">const</span> completionMessages = <span class="hljs-keyword">await</span> getMessagesWithContext(messages, frontend);
  <span class="hljs-keyword">const</span> stream = <span class="hljs-keyword">await</span> streamChatCompletion(completionMessages, MAX_RESPONSE_TOKENS);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(stream);
}
</code></pre>
<h2 id="heading-source-code-1">Source code</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/marcushellberg/vaadin-docs-embeddings">Embeddings generator for the documentation</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/marcushellberg/vaadin-docs-assistant">Frontend app for asking questions</a></p>
</li>
</ul>
<h2 id="heading-acknowledgments">Acknowledgments</h2>
<p>This post and demo app wouldn't have been possible without the excellent insights and resources below:</p>
<ul>
<li><p>Hassan El Mghari's post on Building a <a target="_blank" href="https://vercel.com/blog/gpt-3-app-next-js-vercel-edge-functions">GPT-3 app with Next.js and Vercel Edge Functions</a></p>
</li>
<li><p>Greg Richardson's <a target="_blank" href="https://supabase.com/blog/openai-embeddings-postgres-vector">Storing OpenAI embeddings in Postgres with pgvector</a> and the implementation in the <a target="_blank" href="https://github.com/supabase/supabase">Supabase GitHub repo</a>.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Java 20: a faster future is Looming]]></title><description><![CDATA[The new six-month release cadence that Oracle announced for Java in 2018 marked the beginning of a new era for the language. Instead of getting big, breaking releases every few years, we now receive a steady stream of new features and improvements.
T...]]></description><link>https://marcushellberg.dev/java-20-a-faster-future-is-looming</link><guid isPermaLink="true">https://marcushellberg.dev/java-20-a-faster-future-is-looming</guid><category><![CDATA[Java]]></category><category><![CDATA[vaadin]]></category><dc:creator><![CDATA[Marcus Hellberg]]></dc:creator><pubDate>Thu, 23 Mar 2023 03:13:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/KwJ3FEuwRlE/upload/da55afc65930cfb83e14b34cce0fe566.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The new six-month release cadence that Oracle announced for Java in 2018 marked the beginning of a new era for the language. Instead of getting big, breaking releases every few years, we now receive a steady stream of new features and improvements.</p>
<p>This week, Oracle released Java 20 with some exciting preview features building up to a significant performance boost in the next LTS release, Java 21.</p>
<p>This week, I had the opportunity to attend the Java 20 launch event at Oracle HQ in Redwood City and hear from the team working on the new features.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679540565666/d515aa15-c9ff-4470-b8fd-188c4dbb873f.jpeg" alt="Georges Saab presenting on a stage." class="image--center mx-auto" /></p>
<h2 id="heading-java-a-thoughtful-evolution"><strong>Java: A Thoughtful Evolution</strong></h2>
<p>Georges Saab, Senior VP of Development for the Java Platform, explained that Java's development is a balancing act between conservatism and innovation.</p>
<p>On one hand, they want to maintain compatibility and not alienate people. On the other hand, they want to adapt to change and fix mistakes.</p>
<p>The new release model allows Java to ship new features in smaller increments and make new APIs available to developers earlier. This way, they can receive feedback from the community and make changes before finalizing them.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679540601952/e528a22e-112b-4d5d-bf73-27d135e402b5.jpeg" alt="A graph showing the number of features shipped per release. Releases prior to the new 6-month cycle had tons more changes." class="image--center mx-auto" /></p>
<h2 id="heading-improved-developer-productivity"><strong>Improved Developer Productivity</strong></h2>
<p><a target="_blank" href="https://openjdk.java.net/projects/amber/"><strong>Project Amber</strong></a> is a collection of features aimed at improving developer productivity. The project has already delivered several improvements to the language, such as local variable type inference (<code>var</code>), records, and text blocks.</p>
<p>Java 20 includes previews of two pattern-matching features: <a target="_blank" href="https://openjdk.java.net/jeps/432"><strong>record patterns</strong></a> and <a target="_blank" href="https://openjdk.java.net/jeps/433"><strong>pattern matching for switch</strong></a>.</p>
<p>Record patterns allow you to match the fields of a record, including nested records:</p>
<pre><code class="lang-java"><span class="hljs-keyword">if</span> (<span class="hljs-function">p <span class="hljs-keyword">instanceof</span> <span class="hljs-title">Person</span><span class="hljs-params">(String name, Address(String street, String city, String country)</span>)) </span>{
    <span class="hljs-comment">// do something with name, street, city, country</span>
}

<span class="hljs-keyword">for</span> (Point(<span class="hljs-keyword">var</span> x, <span class="hljs-keyword">var</span> y) : points) {
    <span class="hljs-comment">// do something with x and y</span>
}
</code></pre>
<p>Pattern matching for switch allows you to match on a pattern and cast the value to a specific type, optionally with a guard expression:</p>
<pre><code class="lang-java"><span class="hljs-keyword">switch</span> (o) {
  <span class="hljs-keyword">case</span> String s when s.length() == <span class="hljs-number">1</span> -&gt; ...
  <span class="hljs-keyword">case</span> <span class="hljs-string">"hello"</span> -&gt; ...
  <span class="hljs-keyword">case</span> String s -&gt; ...
  <span class="hljs-keyword">default</span> -&gt; ...
}
</code></pre>
<h2 id="heading-massive-scaling-with-virtual-threads"><strong>Massive Scaling with Virtual Threads</strong></h2>
<p><a target="_blank" href="https://wiki.openjdk.java.net/display/loom/Main"><strong>Project Loom</strong></a> is a major initiative to improve the scalability of Java applications. It introduces a new type of thread called a virtual thread, enabling high-throughput, low-latency, and low-overhead concurrency.</p>
<p>Virtual threads are lightweight and don't require a native thread to be created. This means that you can create millions of virtual threads without running out of native threads. This is especially useful for applications that need to handle a lot of concurrent requests, such as web servers.</p>
<p>The aim of Project Loom is to match the performance of reactive frameworks with a simpler programming model.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679540809365/83f0567b-1b49-4dc5-8308-cbe479bec971.jpeg" alt="Jetty performance using native threads vs virtual threads." class="image--center mx-auto" /></p>
<p><em>Impressively, existing code should just work with virtual threads.</em></p>
<p>Sergey Kuksenko shared five tips for adopting virtual threads:</p>
<ol>
<li><p>Use simple, blocking I/O APIs</p>
</li>
<li><p>Start a new thread per task instead of using shared thread pools</p>
</li>
<li><p>Never pool virtual threads; use semaphores to limit concurrency</p>
</li>
<li><p>Don't cache objects in <code>ThreadLocals</code></p>
</li>
<li><p>Avoid pinning</p>
</li>
</ol>
<h2 id="heading-connecting-the-jvm-with-native-code"><strong>Connecting the JVM with Native Code</strong></h2>
<p>[Project Panama] is a collection of features that allow you to connect the JVM with native code. This is useful for performance-critical applications that need to call native libraries.</p>
<p>Java 20 includes two preview features: <a target="_blank" href="https://openjdk.java.net/projects/panama/"><strong>Foreign Function and Memory API</strong></a> and <a target="_blank" href="https://openjdk.java.net/jeps/426"><strong>Vector API</strong></a>.</p>
<p>While both are APIs that will be used directly by a small number of developers, they can have a significant impact in some areas as they get adopted in libraries and frameworks.</p>
<h2 id="heading-java-21-the-next-lts-release"><strong>Java 21: The Next LTS Release</strong></h2>
<p>The major announcement at the event was that Java 21 will be the next LTS release. Although it wasn't confirmed, it seemed clear that Oracle hopes it will be the first release to include Project Loom.</p>
<h2 id="heading-a-thriving-community"><strong>A Thriving Community</strong></h2>
<p>Java has a strong community behind it, with 10 million Java developers, 60 billion active JVMs (38 billion cloud-based JVMs).</p>
<p>Independent contributors make up the second-largest group of contributors.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679540718492/d9bd6de3-66e4-4c68-b5aa-6d1ca22f96aa.jpeg" alt="OpenJDK contributors. Independent developers are the second-largest group." class="image--center mx-auto" /></p>
<p>It was great to see that Oracle is continuing to invest in growing the community, with initiatives such as the <a target="_blank" href="https://www.oracle.com/java/java-champions-program.html"><strong>Java Champions program</strong></a>. They have also recently hired <a target="_blank" href="https://twitter.com/mono_quito89"><strong>Heather Stephens</strong></a> to head up their Java in education strategy, ensuring that Java remains a relevant language for the next generation of developers.</p>
<h2 id="heading-how-will-the-new-features-affect-vaadin-users"><strong>How Will the New Features Affect Vaadin Users?</strong></h2>
<p>Project Loom has the potential to improve the performance of <a target="_blank" href="https://vaadin.com">Vaadin</a> applications by allowing application servers to handle more concurrent requests. <a target="_blank" href="https://vaadin.com/flow">Vaadin Flow</a> apps are rarely I/O bound, so the benefits of virtual threads are likely to be limited. <a target="_blank" href="https://hilla.dev">Hilla</a> applications, on the other hand, are more likely to benefit from performance improvements.</p>
<p>The new language features will benefit all Java developers, including Vaadin developers.</p>
]]></content:encoded></item><item><title><![CDATA[Tutorial: Set up Google OAuth in a Hilla Spring Boot + React project]]></title><description><![CDATA[In this tutorial, you'll learn how to configure and use Google OAuth authentication in a Hilla project that uses Spring Boot and React.
This tutorial is based on Hilla 2.0 with React.
Source code
You can find the completed source code on GitHub: http...]]></description><link>https://marcushellberg.dev/tutorial-set-up-google-oauth-in-a-hilla-spring-boot-react-project</link><guid isPermaLink="true">https://marcushellberg.dev/tutorial-set-up-google-oauth-in-a-hilla-spring-boot-react-project</guid><category><![CDATA[React]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[Java]]></category><category><![CDATA[Hilla]]></category><dc:creator><![CDATA[Marcus Hellberg]]></dc:creator><pubDate>Wed, 22 Feb 2023 19:57:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677095429463/90cdaccc-acc9-418e-bc3f-0d2422941bb0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this tutorial, you'll learn how to configure and use Google OAuth authentication in a <a target="_blank" href="https://hilla.dev">Hilla</a> project that uses Spring Boot and React.</p>
<p><strong>This tutorial is based on Hilla 2.0 with React</strong>.</p>
<h2 id="heading-source-code"><strong>Source code</strong></h2>
<p>You can find the completed source code on GitHub: <a target="_blank" href="https://github.com/marcushellberg/hilla-google-auth/">https://github.com/marcushellberg/hilla-google-auth/</a></p>
<h2 id="heading-requirements"><strong>Requirements</strong></h2>
<ul>
<li><p>30 minutes</p>
</li>
<li><p>Java 17 or newer</p>
</li>
<li><p>Node 18 or newer</p>
</li>
</ul>
<h2 id="heading-create-a-new-hilla-project-with-a-react-frontend"><strong>Create a new Hilla project with a React frontend</strong></h2>
<p>Generate a new Hilla project with:</p>
<pre><code class="lang-bash">npx @hilla/cli init --react --empty hilla-google-auth
</code></pre>
<h2 id="heading-set-up-a-google-oauth-20-client-id"><strong>Set up a Google OAuth 2.0 Client ID</strong></h2>
<p>To create a client ID, you will first need to create a Google Cloud project, and add an OAuth consent screen to that project. To do this, go to the <a target="_blank" href="https://console.cloud.google.com/apis/credentials/consent"><strong>OAuth consent screen</strong></a> on the Google Cloud Platform dashboard. There you can create a new project and create its OAuth consent screen (select “External” when prompted for the “User Type” of the consent screen).</p>
<p>Once the project with a consent screen is created, go to the <a target="_blank" href="https://console.cloud.google.com/apis/credentials"><strong>credentials page</strong></a> and do the following:</p>
<ol>
<li><p>Select “Create credentials”, followed by “OAuth client ID”.</p>
</li>
<li><p>When prompted for the application type, select “Web application”.</p>
</li>
<li><p>In the “Authorized redirect URIs” field, add a new redirect for <a target="_blank" href="http://localhost:8080/login/oauth2/code/google"><strong>http://localhost:8080/login/oauth2/code/google</strong></a>.</p>
</li>
</ol>
<p>Once the OAuth client is created, take note of the generated client ID and client secret.</p>
<h3 id="heading-note-other-oauth-oidc-providers"><strong>Note: Other OAuth OIDC providers</strong></h3>
<p>You can also use other OAuth social login providers like GitHub, Facebook, and Twitter using these same instructions, replacing "google" in the Spring Security configuration and login paths. You can also configure multiple providers.</p>
<h2 id="heading-add-spring-security-dependencies"><strong>Add Spring Security dependencies</strong></h2>
<p>Begin by adding the following dependencies to the project’s pom.xml file:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.springframework.boot<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>spring-boot-starter-security<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.springframework.boot<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>spring-boot-starter-oauth2-client<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
</code></pre>
<h2 id="heading-configure-spring-security"><strong>Configure Spring Security</strong></h2>
<p>Next, configure and enable Spring Security. Start by creating a new package <code>com.example.application.security</code> and within it a new file <code>SecurityConfiguration.java</code> with the following content:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> com.example.application.security;

<span class="hljs-keyword">import</span> com.vaadin.flow.spring.security.VaadinWebSecurity;
<span class="hljs-keyword">import</span> org.springframework.context.annotation.Configuration;
<span class="hljs-keyword">import</span> org.springframework.security.config.annotation.web.builders.HttpSecurity;
<span class="hljs-keyword">import</span> org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

<span class="hljs-meta">@EnableWebSecurity</span>
<span class="hljs-meta">@Configuration</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SecurityConfiguration</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">VaadinWebSecurity</span> </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">configure</span><span class="hljs-params">(HttpSecurity http)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-keyword">super</span>.configure(http);
        http.oauth2Login()
                .loginPage(<span class="hljs-string">"/login"</span>).permitAll().and()
                .logout().logoutSuccessUrl(<span class="hljs-string">"/"</span>).permitAll();

    }
}
</code></pre>
<p>Then, add the client id and secret you created in the first step to a local <code>application.properties</code> file. <strong>It is important that you don’t commit secrets to Git.</strong></p>
<p>In the root of the project, create a new subdirectory <code>config/local</code> and add it to the <code>.gitignore</code> file.</p>
<pre><code class="lang-bash">mkdir -p config/<span class="hljs-built_in">local</span>
touch config/<span class="hljs-built_in">local</span>/application.properties
<span class="hljs-built_in">echo</span> <span class="hljs-string">"
# Contains local config that shouldn't go into the repository
config/local/"</span> &gt;&gt; .gitignore
</code></pre>
<p>Add the following two properties to the newly created <a target="_blank" href="http://application.properties"><code>application.properties</code></a> file:</p>
<pre><code class="lang-plaintext">spring.security.oauth2.client.registration.google.client-id=&lt;your client id&gt;

spring.security.oauth2.client.registration.google.client-secret=&lt;your client secret&gt;
</code></pre>
<h2 id="heading-create-a-login-page-and-a-view-for-logged-in-users"><strong>Create a login page and a view for logged-in users</strong></h2>
<p>Now that you have Spring Security configured, you can create a login page. The actual login is handled by Google, so all you need is a page that links to the correct URL.</p>
<p>In the frontend/views folder, create a new file <code>LoginView.tsx</code> with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LoginView</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"m-m"</span>&gt;
      &lt;a href=<span class="hljs-string">"/oauth2/authorization/google"</span>&gt;Log <span class="hljs-keyword">in</span> <span class="hljs-keyword">with</span> Google&lt;/a&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>Next, create a view for logged-in users. In the <code>frontend/views</code> folder, create a new file <code>MainView.tsx</code> with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MainView</span>(<span class="hljs-params"></span>) </span>{

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"m-m"</span>&gt;
      &lt;p&gt;You are logged <span class="hljs-keyword">in</span>&lt;/p&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>Finally, update the routing configuration in <code>routes.tsx</code>. Replace the contents of the file with the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createBrowserRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> {LoginView} <span class="hljs-keyword">from</span> <span class="hljs-string">"Frontend/views/LoginView"</span>;
<span class="hljs-keyword">import</span> {MainView} <span class="hljs-keyword">from</span> <span class="hljs-string">"Frontend/views/MainView"</span>;

<span class="hljs-keyword">const</span> router = createBrowserRouter([
  { path: <span class="hljs-string">'/login'</span>, element: &lt;LoginView /&gt; },
  { path: <span class="hljs-string">''</span>, element: &lt;MainView/&gt;}
]);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<h3 id="heading-checkpoint-run-the-application-and-verify-it-works"><strong>✅ Checkpoint: run the application and verify it works</strong></h3>
<p>Run the application and verify that you are able to log in.</p>
<ul>
<li><p>Start the application by running <a target="_blank" href="http://Application.java"><code>Application.java</code></a> in your IDE, or with the included Maven wrapper: <code>./mvnw</code></p>
</li>
<li><p>Spring Security should redirect you to <code>/login</code> when the app starts.</p>
</li>
<li><p>Once you authenticate using Google, you should see <code>MainView</code>.</p>
</li>
</ul>
<h2 id="heading-create-an-endpoint-and-configure-access"><strong>Create an endpoint and configure access</strong></h2>
<p>Create a server endpoint for fetching information about the logged-in user. In the <code>src/main/java/com/example/application/endpoints</code> folder, create a <code>UserDetails.java</code> record to hold information about the user.</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> com.example.application.endpoints;

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">UserDetails</span><span class="hljs-params">(
        String email,
        String name,
        String profilePictureUrl
)</span> </span>{}
</code></pre>
<p>In the same package, create a Hilla endpoint, <code>UserEndpoint.java</code>, with the following content:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> com.example.application.endpoints;

<span class="hljs-keyword">import</span> com.vaadin.flow.spring.security.AuthenticationContext;
<span class="hljs-keyword">import</span> dev.hilla.Endpoint;
<span class="hljs-keyword">import</span> jakarta.annotation.security.PermitAll;
<span class="hljs-keyword">import</span> org.springframework.security.oauth2.core.oidc.user.OidcUser;

<span class="hljs-keyword">import</span> java.util.Optional;

<span class="hljs-meta">@Endpoint</span>
<span class="hljs-meta">@PermitAll</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserEndpoint</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> AuthenticationContext authContext;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">UserEndpoint</span><span class="hljs-params">(AuthenticationContext authContext)</span> </span>{
        <span class="hljs-keyword">this</span>.authContext = authContext;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> Optional&lt;UserDetails&gt; <span class="hljs-title">getAuthenticatedUser</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> authContext.getAuthenticatedUser(OidcUser.class)
                .map(u -&gt; <span class="hljs-keyword">new</span> UserDetails(
                        u.getEmail(),
                        u.getFullName(),
                        u.getPicture()
                ));
    }
}
</code></pre>
<p>Here are the most important parts of the endpoint code:</p>
<ul>
<li><p><code>@Endpoint</code> makes public methods in the class callable from the client through TypeScript methods. It generates corresponding TypeScript types to method parameters and return values.</p>
</li>
<li><p><code>@PermitAll</code> allows all authenticated users to access public methods on the endpoint.</p>
</li>
<li><p>The constructor injects <code>AuthenticationContext</code> to access user details.</p>
</li>
<li><p><code>getAuthenticatedUser()</code> gets the authenticated <code>OidcUser</code> and copies over only the needed info into a <code>UserDetails</code> record.</p>
</li>
</ul>
<h2 id="heading-access-the-authentication-state-from-react-components"><strong>Access the authentication state from React components</strong></h2>
<p>Create a custom React hook and provider for fetching the user details and making them available to components.</p>
<p>In the frontend folder, create a new file <code>useAuth.tsx</code> with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
  createContext,
  ReactNode,
  useContext,
  useEffect,
  useState,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { UserEndpoint } <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/generated/endpoints.js'</span>;
<span class="hljs-keyword">import</span> { logout <span class="hljs-keyword">as</span> serverLogout } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hilla/frontend'</span>;
<span class="hljs-keyword">import</span> UserDetails <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/generated/com/example/application/endpoints/UserDetails.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">authHook</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [authenticated, setAuthenticated] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [user, setUser] = useState&lt;UserDetails&gt;();
  <span class="hljs-keyword">const</span> [authInitialized, setAuthInitialized] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">login</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> authUser = <span class="hljs-keyword">await</span> UserEndpoint.getAuthenticatedUser();
      setUser(authUser);
      setAuthenticated(!!authUser);
    } <span class="hljs-keyword">finally</span> {
      setAuthInitialized(<span class="hljs-literal">true</span>);
    }
  }

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">logout</span>(<span class="hljs-params">redirect: <span class="hljs-built_in">string</span> = '/login'</span>) </span>{
    setAuthenticated(<span class="hljs-literal">false</span>);
    setUser(<span class="hljs-literal">undefined</span>);
    setAuthInitialized(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">await</span> serverLogout();
    location.href = <span class="hljs-string">`<span class="hljs-subst">${location.origin}</span><span class="hljs-subst">${redirect}</span>`</span>;
  }

  useEffect(<span class="hljs-function">() =&gt;</span> {
    login();
  }, []);

  <span class="hljs-keyword">return</span> {
    authenticated,
    authInitialized,
    user,
    login,
    logout,
  };
}

<span class="hljs-keyword">type</span> AuthContextType = ReturnType&lt;<span class="hljs-keyword">typeof</span> authHook&gt;;

<span class="hljs-keyword">const</span> initialValue: AuthContextType = {
  authenticated: <span class="hljs-literal">false</span>,
  user: <span class="hljs-literal">undefined</span>,
  authInitialized: <span class="hljs-literal">false</span>,
  login: <span class="hljs-keyword">async</span> () =&gt; {},
  logout: <span class="hljs-keyword">async</span> () =&gt; {},
};

<span class="hljs-keyword">const</span> AuthContext = createContext&lt;AuthContextType&gt;(initialValue);

<span class="hljs-keyword">interface</span> AuthProviderProps {
  children: ReactNode;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AuthProvider</span>(<span class="hljs-params">{ children }: AuthProviderProps</span>) </span>{
  <span class="hljs-keyword">const</span> auth = authHook();
  <span class="hljs-keyword">return</span> &lt;AuthContext.Provider value={auth}&gt;{children}&lt;/AuthContext.Provider&gt;;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useAuth</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> useContext(AuthContext);
}
</code></pre>
<p>Here are the most important parts:</p>
<ul>
<li><p>The <code>authHook</code> function contains the logic needed for checking the authentication state, accessing user details, and logging out.</p>
</li>
<li><p><code>AuthContext</code> and <code>AuthProvider</code> allow you to have a single, shared, authentication state accessible from any component.</p>
</li>
<li><p><code>useAuth</code> is the hook that gives you access to the auth state and login/logout functionality in your components.</p>
</li>
</ul>
<h2 id="heading-restrict-access-to-views"><strong>Restrict access to views</strong></h2>
<p>Create a component for protecting routes, <code>ProtectedRoute.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ReactElement, ReactNode } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { useAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/useAuth.js'</span>;
<span class="hljs-keyword">import</span> { Navigate, Outlet, useLocation } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> { REDIRECT_PATH_KEY } <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/routes.js'</span>;

<span class="hljs-keyword">interface</span> ProtectedRouteProps {
  redirectPath?: <span class="hljs-built_in">string</span>;
  component: ReactElement;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ProtectedRoute</span>(<span class="hljs-params">{
  redirectPath = '/login',
  component,
}: ProtectedRouteProps</span>): <span class="hljs-title">ReactElement</span> </span>{
  <span class="hljs-keyword">const</span> { authenticated, authInitialized } = useAuth();
  <span class="hljs-keyword">const</span> location = useLocation();

  <span class="hljs-keyword">if</span> (!authInitialized) {
    <span class="hljs-keyword">return</span> &lt;div&gt;&lt;/div&gt;;
  }

  <span class="hljs-keyword">if</span> (!authenticated) {
    <span class="hljs-comment">// Store the requested path, so we can redirect to it after logging in (in App.tsx)</span>
    <span class="hljs-built_in">localStorage</span>.setItem(REDIRECT_PATH_KEY, location.pathname);

    <span class="hljs-keyword">return</span> &lt;Navigate to={redirectPath} replace /&gt;;
  }

  <span class="hljs-keyword">return</span> component ? component : &lt;Outlet /&gt;;
}
</code></pre>
<p>The component:</p>
<ul>
<li><p>Uses the <code>useAuth()</code> hook to access information on the authentication</p>
</li>
<li><p>Displays an empty <code>&lt;div&gt;</code> if auth status is not yet available</p>
</li>
<li><p>If the user is not authenticated, it stores the current path to local storage and redirects to the login page.</p>
</li>
<li><p>If the user is authenticated, it shows the passed-in component or a router outlet</p>
</li>
</ul>
<p>Update <code>routes.tsx</code> to use the new functionality. Replace the contents with the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createBrowserRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> { LoginView } <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/views/LoginView'</span>;
<span class="hljs-keyword">import</span> { MainView } <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/views/MainView'</span>;
<span class="hljs-keyword">import</span> { ProtectedRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/ProtectedRoute.js'</span>;
<span class="hljs-keyword">import</span> { SecretView } <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/views/SecretView.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> REDIRECT_PATH_KEY = <span class="hljs-string">'redirectPath'</span>;

<span class="hljs-keyword">const</span> router = createBrowserRouter([
  { path: <span class="hljs-string">'/login'</span>, element: &lt;LoginView /&gt; },
  { path: <span class="hljs-string">''</span>, element: &lt;ProtectedRoute component={&lt;MainView /&gt;} /&gt; },
]);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>Finally, update <code>App.tsx</code> with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { RouterProvider, useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> router, { REDIRECT_PATH_KEY } <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes'</span>;
<span class="hljs-keyword">import</span> { AuthProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'Frontend/useAuth.js'</span>;
<span class="hljs-keyword">import</span> { useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">/**
   * Redirects a user back to the view they attempted to access before login
   */</span>
  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> redirectPath = <span class="hljs-built_in">localStorage</span>.getItem(REDIRECT_PATH_KEY);

    <span class="hljs-keyword">if</span> (redirectPath) {
      <span class="hljs-built_in">localStorage</span>.removeItem(REDIRECT_PATH_KEY);
      location.href = <span class="hljs-string">`<span class="hljs-subst">${location.origin}</span><span class="hljs-subst">${redirectPath}</span>`</span>;
    }
  }, []);

  <span class="hljs-keyword">return</span> (
    &lt;AuthProvider&gt;
      &lt;RouterProvider router={router} /&gt;
    &lt;/AuthProvider&gt;
  );
}
</code></pre>
<p>The new component:</p>
<ul>
<li><p>Checks if there is a redirect path stored in local storage. If there is, it navigates there.</p>
</li>
<li><p>Wraps the entire application with the <code>AuthProvider</code> to give all components access to the <code>useAuth()</code> hook.</p>
</li>
</ul>
<p>Run the application and verify that you are able to log in.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677095141555/704b4849-591e-46a0-88e1-a3bc81f8305d.png" alt="A browser window displaying a &quot;Log in with Google&quot; link" class="image--center mx-auto" /></p>
<h2 id="heading-display-user-information-and-add-a-logout-button"><strong>Display user information and add a logout button</strong></h2>
<p>Now that you have access to user information through the <code>useAuth()</code> hook, update <code>MainView.tsx</code> to display user information and a logout button.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {useAuth} <span class="hljs-keyword">from</span> <span class="hljs-string">"Frontend/useAuth.js"</span>;
<span class="hljs-keyword">import</span> {Button} <span class="hljs-keyword">from</span> <span class="hljs-string">"@hilla/react-components/Button.js"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MainView</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> {user, logout} = useAuth();

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"m-m"</span>&gt;
      {user &amp;&amp; &lt;div&gt;
          &lt;p&gt;You are logged <span class="hljs-keyword">in</span> <span class="hljs-keyword">as</span> {user.name} ({user.email})&lt;/p&gt;
          &lt;img src={user.profilePictureUrl} alt={user.name} referrerPolicy=<span class="hljs-string">"no-referrer"</span>/&gt;
      &lt;/div&gt;}
      &lt;p&gt;
        &lt;Button onClick={<span class="hljs-function">() =&gt;</span> logout()}&gt;Log out&lt;/Button&gt;
      &lt;/p&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>You should now see information about the logged-in user and be able to log out.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677095181070/8e70c84e-99b5-49ea-975b-542d3844d0ea.png" alt="A browser window displaying the name, email, and picture of the logged-in user." class="image--center mx-auto" /></p>
<h2 id="heading-a-note-on-logout"><strong>A note on logout</strong></h2>
<p>In this example, logging out invalidates the session and logs the user out from only this application. When using a social login like Google, it is not expected behavior to log the user out of all Google services, like Gmail, when logging out.</p>
<p>In some cases, you may want to log the user out of the SSO provider. In that case, you need to perform an RP-initiated logout. Read more about handling Open ID Connect logouts <a target="_blank" href="https://developer.okta.com/blog/2020/03/27/spring-oidc-logout-options">in this blog post from Okta</a>.</p>
<h2 id="heading-completed-application-source-code"><strong>Completed application source code</strong></h2>
<p>You can find the source code for the completed application on GitHub:</p>
<p><a target="_blank" href="https://github.com/marcushellberg/hilla-google-auth/">https://github.com/marcushellberg/hilla-google-auth/</a></p>
]]></content:encoded></item><item><title><![CDATA[Avoiding stale state in React useEffect]]></title><description><![CDATA[Sometimes it's easy for me to forget I'm using JavaScript when building React apps.
I've been using Lit for a long time. In Lit, it's plainly clear that I'm using JavaScript and working with the DOM. In React, that gets abstracted away. But it doesn'...]]></description><link>https://marcushellberg.dev/avoiding-stale-state-in-react-useeffect</link><guid isPermaLink="true">https://marcushellberg.dev/avoiding-stale-state-in-react-useeffect</guid><category><![CDATA[Hilla]]></category><category><![CDATA[React]]></category><category><![CDATA[ReactHooks]]></category><dc:creator><![CDATA[Marcus Hellberg]]></dc:creator><pubDate>Sat, 07 Jan 2023 18:34:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/xkBaqlcqeb4/upload/d021710f543df0d8d2dce3b7182baff2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sometimes it's easy for me to forget I'm using JavaScript when building React apps.</p>
<p>I've been using <a target="_blank" href="https://lit.dev/">Lit</a> for a long time. In Lit, it's plainly clear that I'm using JavaScript and working with the DOM. In React, that gets abstracted away. But it doesn't change the fact that the rules of JavaScript still apply.</p>
<h2 id="heading-why-is-my-state-getting-reset-on-every-useeffect-call-in-react">Why is my state getting reset on every useEffect call in React?</h2>
<p>I'm building a chat application for an upcoming <a target="_blank" href="https://hilla.dev/">Hilla</a> presentation. I want to subscribe to incoming messages through a web socket, and I only want to set up the connection once when the component gets mounted.</p>
<p>This was my initial attempt:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatView</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [messages, setMessages] = useState&lt;Message[]&gt;([]);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> subscription = ChatService
      .join()
      .onNext(<span class="hljs-function"><span class="hljs-params">message</span> =&gt;</span> 
        setMessages([...messages, message]));

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      subscription.cancel();
    };
  }, []);

  <span class="hljs-keyword">return</span> (
    <span class="hljs-comment">// view code</span>
  )
}
</code></pre>
<p>Looks straightforward enough: subscribe to the service, update the messages array on incoming messages, and close the subscription on unmount. Only run the effect once by passing in an empty deps array <code>[]</code>.</p>
<p>The problem? My message list only contained the latest message instead of the full chat history.</p>
<p><strong>After a few minutes of debugging, it dawned on me:</strong> <em>because the effect only runs once, the value of</em> <code>messages</code> <em>in the closure is still the value it had when the component was created</em>.</p>
<h2 id="heading-updating-state-from-a-useeffect-hook">Updating state from a useEffect hook</h2>
<p>The solution was simple. To update a state that's dependent on the previous value of the state, you need to do a <a target="_blank" href="https://reactjs.org/docs/hooks-reference.html#functional-updates">functional update</a> by passing in a function to setState. The function gets the previous state as a parameter, which avoids the stale state issue.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatView</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [messages, setMessages] = useState&lt;Message[]&gt;([]);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> subscription = ChatService
      .join()
      .onNext(<span class="hljs-function"><span class="hljs-params">message</span> =&gt;</span>
        setMessages(<span class="hljs-function"><span class="hljs-params">prev</span> =&gt;</span> [...prev, message]));

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      subscription.cancel();
    };
  }, []);

  <span class="hljs-keyword">return</span> (
    <span class="hljs-comment">// view code</span>
  )
}
</code></pre>
<p>That's it!</p>
<p>You can find the full code for my demo on my GitHub <a target="_blank" href="https://github.com/marcushellberg/hilla-react-chat">https://github.com/marcushellberg/hilla-react-chat</a></p>
]]></content:encoded></item></channel></rss>